From bfe73b3370854ad33be59c120eb4cec012d431dc Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:54:48 +0200 Subject: [PATCH 001/153] add pdfmake library #TASK-4860 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6535f46a65..ce523bc3e4 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "eonasdan-bootstrap-datetimepicker": "~4.17.43", "file-saver": "~1.3.2", "highcharts": "^8.0.4", + "html-to-pdfmake": "^2.4.23", "jquery": "~2.2.4", "jquery.json-viewer": "^1.4.0", "jszip": "^3.10.1", @@ -72,7 +73,7 @@ "lodash": "^4.17.19", "moment": "^2.15.1", "pako": "~0.2.8", - "pdfmake": "~0.1.20", + "pdfmake": "^0.2.7", "qtip2": "~3.0.3", "select2": "^4.1.0-rc.0", "select2-bootstrap-theme": "0.1.0-beta.10", From 52066c836dc21b169130238ac380ba3865acc290 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 18 Aug 2023 14:29:30 +0200 Subject: [PATCH 002/153] add pdfmake libraries to index.html #TASK-4860 --- src/sites/iva/index.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sites/iva/index.html b/src/sites/iva/index.html index 1fdb6e1a9d..ed496ace35 100644 --- a/src/sites/iva/index.html +++ b/src/sites/iva/index.html @@ -117,6 +117,10 @@ + + + + From 24dc793ad42987765c9f558a0b4e5e34d65883a4 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 18 Aug 2023 14:32:12 +0200 Subject: [PATCH 003/153] wc: implement first pdf export #TASK-4860 --- src/core/pdf-builder.js | 73 +++++++++++++++++++ .../clinical/clinical-analysis-review.js | 16 ++++ .../interpretation/variant-interpreter.js | 3 +- 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/core/pdf-builder.js diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js new file mode 100644 index 0000000000..2685eb169a --- /dev/null +++ b/src/core/pdf-builder.js @@ -0,0 +1,73 @@ + + +export const createPdf = docDefinition => ({ + doc: { + pageSize: "A4", + styles: { + header: { + fontSize: 12, + bold: true, + }, + subheader: { + fontSize: 11, + bold: true, + }, + label: { + bold: true + }, + small: { + fontSize: 9 + } + }, + defaultStyle: { + fontSize: 10 + }, + ...docDefinition, + }, + tableLayout: { + headerVerticalBlueLine: { + // top & bottom + hLineWidth: function () { + return 0; + }, + // left & right + vLineWidth: function (i, node) { + // i == 0 mean no draw line on start + // i == node.table.body.length no draw the last line + if (i === node.table.body.length) { + return 0; + } + // it will draw a line if i == 0 + return i === 0 ? 2 : 0; + }, + vLineColor: function (i) { + return i === 0 ? "#0c2f4c" : ""; + }, + fillColor: function () { + return "#f3f3f3"; + } + }, + }, + open() { + pdfMake.createPdf(this.doc, this.table).open(); + }, + download() { + pdfMake.createPdf(this.doc).download(); + }, + print() { + pdfMake.createPdf(this.doc).print(); + }, + pdfBlob() { + return new Promise((resolve, reject) => { + pdfMake.createPdf(this.doc, this.table) + .getBlob(result =>{ + resolve(result); + }, + err =>{ + reject(err); + }); + }); + }, +}); + + diff --git a/src/webcomponents/clinical/clinical-analysis-review.js b/src/webcomponents/clinical/clinical-analysis-review.js index fb2f5503b6..2180fa5bf2 100644 --- a/src/webcomponents/clinical/clinical-analysis-review.js +++ b/src/webcomponents/clinical/clinical-analysis-review.js @@ -21,6 +21,7 @@ import LitUtils from "../commons/utils/lit-utils.js"; import ClinicalAnalysisManager from "./clinical-analysis-manager.js"; import FormUtils from "../commons/forms/form-utils.js"; import NotificationUtils from "../commons/utils/notification-utils.js"; +import {createPdf} from "../../core/pdf-builder.js"; import "./clinical-analysis-summary.js"; import "../variant/interpretation/variant-interpreter-grid.js"; import "../disease-panel/disease-panel-grid.js"; @@ -332,12 +333,27 @@ export default class ClinicalAnalysisReview extends LitElement { } } + onDownloadPdf() { + const pdfDocument = createPdf({ + content: [ + "First paragraph", + "Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines" + ] + }); + pdfDocument.open(); + } + render() { if (!this.clinicalAnalysis) { return ""; } return html` + ${this.clinicalAnalysis.interpretation?.method?.name ? html`
- ${this.clinicalAnalysis.interpretation.method.name} + ${this.clinicalAnalysis.interpretation.method.name}
` : null}
From 5d323b17927cbcc96e04261f2123c56cec1dc7a7 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Tue, 22 Aug 2023 12:59:20 +0200 Subject: [PATCH 004/153] wc: improvements pdfBuilder #TASK-4860 --- src/core/pdf-builder.js | 329 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 297 insertions(+), 32 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 2685eb169a..54791cb560 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -1,30 +1,53 @@ +import UtilsNew from "./utils-new"; -export const createPdf = docDefinition => ({ - doc: { - pageSize: "A4", - styles: { - header: { - fontSize: 12, - bold: true, - }, - subheader: { - fontSize: 11, - bold: true, - }, - label: { - bold: true - }, - small: { - fontSize: 9 - } +export default class PdfBuilder { + + // https://pdfmake.github.io/docs/0.1/document-definition-object/styling/#style-properties + /** + * @typedef {Object} Style + * @property {string} font - name of the font + * @property {number} fontSize - size of the font in pt + * @property {string[]} fontFeatures - array of advanced typographic features supported in TTF fonts (supported features depend on font file) + * @property {number} lineHeight - the line height (default: 1) + * @property {boolean} bold - whether to use bold text (default: false) + * @property {boolean} italics - whether to use italic text (default: false) + * @property {string} alignment - (‘left’ or ‘center’ or ‘right’ or ‘justify’) the alignment of the text + * @property {number} characterSpacing - size of the letter spacing in pt + * @property {string} color - the color of the text (color name e.g., ‘blue’ or hexadecimal color e.g., ‘#ff5500’) + * @property {string} background - the background color of the text + * @property {string} markerColor - the color of the bullets in a buletted list + * @property {string} decoration - the text decoration to apply (‘underline’ or ‘lineThrough’ or ‘overline’) + * @property {string} decorationStyle - the style of the text decoration (‘dashed’ or ‘dotted’ or ‘double’ or ‘wavy’) + * @property {string} decorationColor - the color of the text decoration, see color + **/ + + /** + * @param {Style} style - Define the style config you want to use for the element + * @returns {Style} return style + */ + styleObject(style) { + return {...style}; + } + + stylesDefault = { + header: { + fontSize: 22, + bold: true, + }, + subheader: { + fontSize: 11, + bold: true, }, - defaultStyle: { - fontSize: 10 + label: { + bold: true }, - ...docDefinition, - }, - tableLayout: { + small: { + fontSize: 9 + } + }; + + tableLayoutDefault = { headerVerticalBlueLine: { // top & bottom hLineWidth: function () { @@ -47,16 +70,39 @@ export const createPdf = docDefinition => ({ return "#f3f3f3"; } }, - }, - open() { - pdfMake.createPdf(this.doc, this.table).open(); - }, - download() { + }; + + constructor(data, docDefinition) { + this.#init(data, docDefinition); + } + + // docDefinition Config + #init(data, dataFormConfig) { + this.data = data ?? {}; + this.docDefinitionConfig = dataFormConfig; + this.docDefinition = { + pageSize: "A4", + styles: {...this.stylesDefault, ...this.docDefinitionConfig?.styles}, + defaultStyle: { + fontSize: 10 + }, + watermaker: {...this.docDefinitionConfig.watermaker}, + content: [this.docDefinitionConfig.sections ? this.#transformData() : []] + }; + } + + exportToPdf() { + pdfMake.createPdf(this.docDefinition, this.tableLayoutDefault).open(); + } + + downloadPdf() { pdfMake.createPdf(this.doc).download(); - }, + } + print() { pdfMake.createPdf(this.doc).print(); - }, + } + pdfBlob() { return new Promise((resolve, reject) => { pdfMake.createPdf(this.doc, this.table) @@ -67,7 +113,226 @@ export const createPdf = docDefinition => ({ reject(err); }); }); - }, -}); + } + + #getBooleanValue(value, defaultValue) { + let _value = typeof defaultValue !== "undefined" ? defaultValue : true; + + if (typeof value !== "undefined" && value !== null) { + return _value = value; + } + + if (typeof _value === "function") { + return _value = value(this.data); + } + + console.error(`Expected boolean or function value, but got ${typeof value}`); + } + + #getVisibleSections() { + return this.docDefinitionConfig.sections + .filter(section => section.elements[0].type !== "notification" || section.elements.length > 1) + .filter(section => this.#getBooleanValue(section?.display?.visible, true)); + } + + /** + * @param {string} field - + * @param {string} defaultValue -- + * @returns {Any} return style + */ + #getValue(field, defaultValue) { + return UtilsNew.getObjectValue(this.data, field, defaultValue); + } + + #transformData() { + return this.#getVisibleSections() + .map(section => this.#writePdfSection(section)); + } + + #writePdfElement(element, section) { + const content = []; + switch (element?.type) { + case "text": + content.push(this.#creatTextElement(element)); + break; + case "label": + content.push(this.#createLabelElement(element)); + break; + case "table": + content.push(this.#createTableElement(element)); + break; + case "column": // layout + content.push(this.#createColumnElement(element)); + break; + case "list": + content.push(this.#createListElement(element)); + break; + case "custom": + content.push(this.#createHtmlElement(element)); + break; + default: + throw new Error("Element type not supported:" + element?.type); + } + return content; + } + + #writePdfSection(section) { + return { + stack: [ + this.#createTitleElement({text: section.title, display: section.display}), + section.elements.map(element => this.#writePdfElement(element, section)) + ] + }; + } + + #creatTextElement(element) { + const value = element?.text || ""; + const classes = element.display?.classes || {}; + const propsStyle = element.display?.propsStyle || {}; + return { + text: value, + style: classes, + ...propsStyle + }; + } + + #createLabelElement(element) { + const labelStyleDefault = { + bold: true + }; + + return { + text: [ + this.#creatTextElement({text: element?.title + ":", ...labelStyleDefault}), + this.#creatTextElement({text: this.#getValue(element.field, element?.defaultValue || "")}) + ] + }; + } + + #createTitleElement(element) { + const titleStyleDefault = { + propsStyle: { + bold: true + } + }; + return { + ...this.#creatTextElement({ + ...element, + display: { + classes: "header", + ...titleStyleDefault, + } + }) + }; + } + + // #createStackElement(elements) { + // return { + // stack: [ + // elements.map(element => this.#writePdfElement(element)) + // ], + // }; + // } + + #createTableElement(element) { + const cols = element.display.columns; + // Initialize headers + const headIndex = []; // Store column indices that need to be skipped + const headers = cols.map(columns => { + const head = []; + columns.forEach((column, index) => { + if (headIndex.length > 0 && column?.rowspan === 1 && column?.colspan === 1) { + // Fill in empty cells for skipped columns + head.push(...Array(headIndex[0]).fill("")); + headIndex.shift(); + } + + // Create a cell with the specified properties + const cell = { + content: column.title, + display: { + bold: true, + colSpan: column.colspan, + rowSpan: column.rowspan + } + }; + + head.push(this.text(cell)); + + if (column?.colspan > 1) { + // Store the index of the column to skip + headIndex.push(index); + head.push({}); + } + }); + + return head; + }); + + // Initialize rows + const array = this.#getValue(element.field, element?.defaultValue); + const rows = array.map(item => { + const row = []; + let colIndex = 0; + const columnSubHeader = cols[1]; + + cols[0].forEach(column => { + const colFormatter = column?.formatter; + const fieldValue = item[column.field]; + + if (column.colspan > 1) { + // For columns with colspan > 1, loop through the sub-columns + colIndex += column.colspan; + for (let i = colIndex - column.colspan; i < colIndex; i++) { + const subHeaderFormatter = columnSubHeader[i]?.formatter; + // Apply sub-header formatter if it exists, otherwise use fieldValue + row.push(subHeaderFormatter ? subHeaderFormatter(fieldValue, item) : fieldValue); + } + } else { + // For single column, apply column formatter if it exists, otherwise use fieldValue + row.push(colFormatter ? colFormatter(fieldValue, item) : fieldValue); + } + }); + + // Add the completed row to the rows array + return row; + }); + + // Merge headers and rows + const contents = [...headers, ...rows]; + + return { + table: { + headerRows: cols.length, + body: contents, + }, + }; + } + + #createColumnElement(element) { + const cols = element.display.columns; + return { + columns: [...cols] + }; + } + + #createListElement(element) { + const array = this.#getValue(element.field, element?.defaultValue || []); + // ol or ul + const contentLayout = element.display?.contentLayout === "bullets" ? "ul" : "ol"; + return { + [contentLayout]: [...array] + }; + } + #createHtmlElement(element) { + const data = element?.field ? this.#getValue(element?.field, {}) : this.data; + const content = element.display?.render ? element.display?.render(data) : {}; + const htmlStyleDefault = { + removeExtraBlanks: true, + ignoreStyles: ["font-family"] + }; + return htmlToPdfmake(content, {...htmlStyleDefault}); + } +} From 5ea739cf6a495fe624b8268eb64f25e23a5e7527 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:01:34 +0200 Subject: [PATCH 005/153] wc: add export as pdf sample-view #TASK-4860 --- src/webcomponents/sample/sample-view.js | 130 ++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index eb9e58ae7a..9b45226917 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -23,6 +23,7 @@ import "../commons/forms/data-form.js"; import "../commons/filters/catalog-search-autocomplete.js"; import "../study/annotationset/annotation-set-view.js"; import "../loading-spinner.js"; +import PdfBuilder from "../../core/pdf-builder.js"; export default class SampleView extends LitElement { @@ -119,6 +120,130 @@ export default class SampleView extends LitElement { this.sampleId = e.detail.value; } + onDownloadPdf() { + const dataFormConfig = { + title: "Summary", + icon: "", + display: this.displayConfig || this.displayConfigDefault, + sections: [ + { + title: "General", + collapsed: false, + display: { + visible: sample => sample?.id, + }, + elements: [ + { + title: "Sample ID", + type: "custom", + display: { + visible: sample => sample?.id, + render: data => `${data.id} (UUID: ${data.uuid})`, + }, + }, + { + title: "Individual ID", + field: "individualId", + type: "label", + }, + { + title: "Files", + field: "fileIds", + type: "list", + display: { + defaultValue: "Files not found or empty", + contentLayout: "bullets", + }, + }, + { + title: "Somatic", + field: "somatic", + type: "label", + display: { + defaultValue: "false", + }, + }, + { + title: "Version", + field: "version", + type: "label", + }, + { + title: "Status", + field: "internal.status", + type: "custom", + display: { + render: field => `${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, + }, + }, + { + title: "Creation Date", + field: "creationDate", + type: "custom", + display: { + render: field => UtilsNew.dateFormatter(field), + }, + }, + { + title: "Modification Date", + field: "modificationDate", + type: "custom", + display: { + render: field => UtilsNew.dateFormatter(field), + }, + }, + { + title: "Description", + field: "description", + type: "label", + defaultValue: "N/A", + }, + // { + // title: "Phenotypes", + // field: "phenotypes", + // type: "list", + // defaultValue: "N/A", + // display: { + // contentLayout: "bullets", + // render: phenotype => { + // // let id = phenotype?.id; + // // if (phenotype?.id?.startsWith("HP:")) { + // // id = html` + // // + // // ${phenotype.id} + // // + // // `; + // // } + // return phenotype?.name ? `${phenotype.name} (${id})}` : `${id}`; + // }, + // }, + // }, + /* + { + title: "Annotation sets", + field: "annotationSets", + type: "custom", + display: { + render: field => html`` + } + } + */ + ], + }, + ], + }; + // watermark: { + // text: "Draft Report", + // color: "blue", + // opacity: 0.3, + // bold: true, + // italics: false + // }, + // content: [] + const pdfDocument = new PdfBuilder(this.sample, dataFormConfig); + pdfDocument.exportToPdf(); + } + render() { if (this.isLoading) { return html``; @@ -134,6 +259,11 @@ export default class SampleView extends LitElement { } return html` + From 5f946af144380ab5ea6c661ddf5ffee0c5d41763 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:16:02 +0200 Subject: [PATCH 006/153] wc: add default styles pdfBuilder #TASK-4860 --- src/core/pdf-builder.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 54791cb560..6ec8cedefb 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -31,19 +31,38 @@ export default class PdfBuilder { } stylesDefault = { - header: { - fontSize: 22, + h1: { + fontSize: 24, + bold: true, + }, + h2: { + fontSize: 20, + bold: true, + }, + h3: { + fontSize: 18, + bold: true, + }, + h4: { + fontSize: 16, bold: true, }, subheader: { - fontSize: 11, + fontSize: 14, bold: true, }, + body: { + fontSize: 12, + }, label: { + fontSize: 12, bold: true }, - small: { - fontSize: 9 + caption: { + fontSize: 8 + }, + note: { + fontSize: 10 } }; From 2e74c85f2c741d9347297ad75b9c349a777c77e7 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:16:58 +0200 Subject: [PATCH 007/153] wc: fix getBooleanValue pdfBuilder #TASK-4860 --- src/core/pdf-builder.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 6ec8cedefb..1d4e2a50e1 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -135,23 +135,27 @@ export default class PdfBuilder { } #getBooleanValue(value, defaultValue) { - let _value = typeof defaultValue !== "undefined" ? defaultValue : true; + let _defaultValue = typeof defaultValue !== "undefined" ? defaultValue : true; if (typeof value !== "undefined" && value !== null) { - return _value = value; - } + if (typeof value === "boolean") { + _defaultValue = value; + } - if (typeof _value === "function") { - return _value = value(this.data); + if (typeof value === "function") { + return value(this.data); + } else { + console.error(`Expected boolean or function value, but got ${typeof value}`); + } } + return _defaultValue; - console.error(`Expected boolean or function value, but got ${typeof value}`); } #getVisibleSections() { return this.docDefinitionConfig.sections .filter(section => section.elements[0].type !== "notification" || section.elements.length > 1) - .filter(section => this.#getBooleanValue(section?.display?.visible, true)); + .filter(section => this.#getBooleanValue(section?.displaySection?.visible, true)); } /** From c53434d9f25dacb73aefc4173cd0c49d5b1fed74 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:19:09 +0200 Subject: [PATCH 008/153] wc: Improvements in PDF Element Creation #TASK-4860 --- src/core/pdf-builder.js | 57 +++++++++++++++---------- src/webcomponents/sample/sample-view.js | 14 +++--- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 1d4e2a50e1..c7ba886d5c 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -200,11 +200,20 @@ export default class PdfBuilder { } #writePdfSection(section) { + const titleStyleDefault = { + classes: "h1" + }; return { stack: [ - this.#createTitleElement({text: section.title, display: section.display}), + this.#creatTextElement({text: section.title, + display: { + ...titleStyleDefault, + ...section?.display + } + }), section.elements.map(element => this.#writePdfElement(element, section)) - ] + ], + ...section?.displaySection }; } @@ -221,34 +230,23 @@ export default class PdfBuilder { #createLabelElement(element) { const labelStyleDefault = { - bold: true + propsStyle: { + bold: true + } }; return { text: [ - this.#creatTextElement({text: element?.title + ":", ...labelStyleDefault}), + this.#creatTextElement({ + text: `${element?.title}: `, + display: { + ...labelStyleDefault + }}), this.#creatTextElement({text: this.#getValue(element.field, element?.defaultValue || "")}) ] }; } - #createTitleElement(element) { - const titleStyleDefault = { - propsStyle: { - bold: true - } - }; - return { - ...this.#creatTextElement({ - ...element, - display: { - classes: "header", - ...titleStyleDefault, - } - }) - }; - } - // #createStackElement(elements) { // return { // stack: [ @@ -343,8 +341,23 @@ export default class PdfBuilder { const array = this.#getValue(element.field, element?.defaultValue || []); // ol or ul const contentLayout = element.display?.contentLayout === "bullets" ? "ul" : "ol"; + // return { + // [contentLayout]: [...array] + // }; + const labelStyleDefault = { + propsStyle: { + bold: true + } + }; return { - [contentLayout]: [...array] + stack: [ + this.#creatTextElement({ + text: `${element?.title}`, + display: { + ...labelStyleDefault + }}), + {[contentLayout]: [...array]} + ] }; } diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 9b45226917..93ea14ea40 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -128,9 +128,11 @@ export default class SampleView extends LitElement { sections: [ { title: "General", - collapsed: false, display: { - visible: sample => sample?.id, + classes: "h1", + }, + displaySection: { + // visible: sample => sample?.id, }, elements: [ { @@ -138,7 +140,7 @@ export default class SampleView extends LitElement { type: "custom", display: { visible: sample => sample?.id, - render: data => `${data.id} (UUID: ${data.uuid})`, + render: data => `
${data.id} (UUID: ${data.uuid})
`, }, }, { @@ -173,7 +175,7 @@ export default class SampleView extends LitElement { field: "internal.status", type: "custom", display: { - render: field => `${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, + render: field => `
${field?.name} (${UtilsNew.dateFormatter(field?.date)})
`, }, }, { @@ -181,7 +183,7 @@ export default class SampleView extends LitElement { field: "creationDate", type: "custom", display: { - render: field => UtilsNew.dateFormatter(field), + render: field => `
${UtilsNew.dateFormatter(field)}
`, }, }, { @@ -189,7 +191,7 @@ export default class SampleView extends LitElement { field: "modificationDate", type: "custom", display: { - render: field => UtilsNew.dateFormatter(field), + render: field => `
${UtilsNew.dateFormatter(field)}
`, }, }, { From e9267ab561850698763db5aa35a0cd447f35ce9c Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:30:55 +0200 Subject: [PATCH 009/153] wc: add display.showPDF config #TASK-4860 --- src/core/pdf-builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index c7ba886d5c..02ad491538 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -155,7 +155,7 @@ export default class PdfBuilder { #getVisibleSections() { return this.docDefinitionConfig.sections .filter(section => section.elements[0].type !== "notification" || section.elements.length > 1) - .filter(section => this.#getBooleanValue(section?.displaySection?.visible, true)); + .filter(section => this.#getBooleanValue((section?.display?.visible) && (section?.display?.showPDF)), false); } /** From 483845e4cd314ae7e1c0314420a218b89ef2abad Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:31:33 +0200 Subject: [PATCH 010/153] wc: fix getValue boolean type #TASK-4860 --- src/core/pdf-builder.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 02ad491538..419f2be4a8 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -164,7 +164,11 @@ export default class PdfBuilder { * @returns {Any} return style */ #getValue(field, defaultValue) { - return UtilsNew.getObjectValue(this.data, field, defaultValue); + const _value = UtilsNew.getObjectValue(this.data, field, defaultValue); + if (typeof _value === "boolean") { + return _value.toString(); + } + return _value; } #transformData() { From de739d084d0c5672c6ae4a4cffd6d1c2f4e850d8 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:32:22 +0200 Subject: [PATCH 011/153] wc: improvements pdfBuilder #TASK-4860 --- src/core/pdf-builder.js | 56 +++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 419f2be4a8..6164e84b7f 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -176,29 +176,33 @@ export default class PdfBuilder { .map(section => this.#writePdfSection(section)); } - #writePdfElement(element, section) { + #writePdfElement(element) { const content = []; - switch (element?.type) { - case "text": - content.push(this.#creatTextElement(element)); - break; - case "label": - content.push(this.#createLabelElement(element)); - break; - case "table": - content.push(this.#createTableElement(element)); - break; - case "column": // layout - content.push(this.#createColumnElement(element)); - break; - case "list": - content.push(this.#createListElement(element)); - break; - case "custom": - content.push(this.#createHtmlElement(element)); - break; - default: - throw new Error("Element type not supported:" + element?.type); + if (!element.type) { + content.push(this.#createLabelElement(element)); + } else { + switch (element?.type) { + case "text": + content.push(this.#creatTextElement(element)); + break; + case "label": + content.push(this.#createLabelElement(element)); + break; + case "table": + content.push(this.#createTableElement(element)); + break; + case "column": // layout + content.push(this.#createColumnElement(element)); + break; + case "list": + content.push(this.#createListElement(element)); + break; + case "custom": + content.push(this.#createHtmlElement(element)); + break; + default: + throw new Error("Element type not supported:" + element?.type); + } } return content; } @@ -212,12 +216,11 @@ export default class PdfBuilder { this.#creatTextElement({text: section.title, display: { ...titleStyleDefault, - ...section?.display } }), section.elements.map(element => this.#writePdfElement(element, section)) ], - ...section?.displaySection + ...section?.display }; } @@ -367,12 +370,15 @@ export default class PdfBuilder { #createHtmlElement(element) { const data = element?.field ? this.#getValue(element?.field, {}) : this.data; + const title = element?.title; const content = element.display?.render ? element.display?.render(data) : {}; const htmlStyleDefault = { removeExtraBlanks: true, ignoreStyles: ["font-family"] }; - return htmlToPdfmake(content, {...htmlStyleDefault}); + + const container = `
${title ? title + ": " : ""}${content}
`; + return htmlToPdfmake(container, {...htmlStyleDefault}); } } From 7303090da76cbc5f63a341e74d4020c895983743 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:34:40 +0200 Subject: [PATCH 012/153] wc: remove lit-html on getDefaultConfig custom type #TASK-4860 --- src/webcomponents/commons/forms/data-form.js | 150 ++++++++++--------- src/webcomponents/sample/sample-view.js | 67 ++++----- 2 files changed, 109 insertions(+), 108 deletions(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 90278458fb..bb7cd5894b 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -385,31 +385,31 @@ export default class DataForm extends LitElement { return html`
${this.config?.display.layout.map(section => { - const sectionClassName = section.className ?? section.classes ?? ""; - const sectionStyle = section.style ?? ""; + const sectionClassName = section.className ?? section.classes ?? ""; + const sectionStyle = section.style ?? ""; - if (section.id) { - return html` + if (section.id) { + return html`
${this._createSection(this.config.sections.find(s => s.id === section.id))}
`; - } else { - return html` + } else { + return html`
${(section.sections || []).map(subsection => { - const subsectionClassName = subsection.className ?? subsection.classes ?? ""; - const subsectionStyle = subsection.style ?? ""; - return subsection.id && html` + const subsectionClassName = subsection.className ?? subsection.classes ?? ""; + const subsectionStyle = subsection.style ?? ""; + return subsection.id && html`
${this._createSection(this.config.sections.find(s => s.id === subsection.id))}
`; - })} + })}
`; - } - })} + } + })}
`; } else { @@ -546,7 +546,7 @@ export default class DataForm extends LitElement { content = this._createTreeElement(element); break; case "custom": - content = this._createCustomElement(element); + content = html`${this._createCustomElement(element)}`; break; case "download": content = this._createDownloadElement(element); @@ -1061,32 +1061,32 @@ export default class DataForm extends LitElement { ` : null} ${array - .map(row => html` + .map(row => html` ${element.display.columns - .map(elem => { - const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; - const elemStyle = elem.display?.style ?? ""; - let content = null; - - // Check the element type - switch (elem.type) { - case "complex": - content = this._createComplexElement(elem, row); - break; - case "custom": - content = elem.display?.render && elem.display.render(this.getValue(elem.field, row)); - break; - default: - content = this.getValue(elem.field, row, elem.defaultValue, elem.format); - } - - return html` + .map(elem => { + const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; + const elemStyle = elem.display?.style ?? ""; + let content = null; + + // Check the element type + switch (elem.type) { + case "complex": + content = this._createComplexElement(elem, row); + break; + case "custom": + content = elem.display?.render && elem.display.render(this.getValue(elem.field, row)); + break; + default: + content = this.getValue(elem.field, row, elem.defaultValue, elem.format); + } + + return html` ${content} `; - })} + })} `)} @@ -1225,7 +1225,9 @@ export default class DataForm extends LitElement { // Call to render function, it must be defined! // We also allow to call to 'onFilterChange' function. - const content = element.display.render(data, value => this.onFilterChange(element, value), this.updateParams, this.data, item); + const contentHTML = element.display.render(data, value => this.onFilterChange(element, value), this.updateParams, this.data, item); + // unsafeHTML or utilsNew.renderHTML + const content = typeof contentHTML === "string" ? UtilsNew.renderHTML(contentHTML) : contentHTML; if (content) { return this._createElementTemplate(element, data, content); } else { @@ -1336,38 +1338,38 @@ export default class DataForm extends LitElement { const view = html`
${items?.slice(0, maxNumItems) - .map((item, index) => { - const _element = JSON.parse(JSON.stringify(element)); - // We create 'virtual' element fields: phenotypes[].1.id, by doing this all existing - // items have a virtual element associated, this will allow to get the proper value later. - for (let i = 0; i< _element.elements.length; i++) { - // This support object nested - const [left, right] = _element.elements[i].field.split("[]."); - _element.elements[i].field = left + "[]." + index + "." + right; - if (_element.elements[i].type === "custom") { - _element.elements[i].display.render = element.elements[i].display.render; - } - if (_element.elements[i].type === "select" && typeof element.elements[i].allowedValues === "function") { - _element.elements[i].allowedValues = element.elements[i].allowedValues; - } - if (typeof element.elements[i]?.validation?.validate === "function") { - _element.elements[i].validation.validate = element.elements[i].validation.validate; - } - if (typeof element.elements[i]?.save === "function") { - _element.elements[i].save = element.elements[i].save; - } - // if (typeof element.elements[i]?.validation?.message === "function") { - // _element.elements[i].validation.message = element.elements[i].validation.message; - // } - // Copy JSON stringify and parse ignores functions, we need to copy them - if (typeof element.elements[i]?.display?.disabled === "function") { - _element.elements[i].display.disabled = element.elements[i].display.disabled; - } - if (typeof element.elements[i]?.display?.visible === "function") { - _element.elements[i].display.visible = element.elements[i].display.visible; - } - } - return html` + .map((item, index) => { + const _element = JSON.parse(JSON.stringify(element)); + // We create 'virtual' element fields: phenotypes[].1.id, by doing this all existing + // items have a virtual element associated, this will allow to get the proper value later. + for (let i = 0; i< _element.elements.length; i++) { + // This support object nested + const [left, right] = _element.elements[i].field.split("[]."); + _element.elements[i].field = left + "[]." + index + "." + right; + if (_element.elements[i].type === "custom") { + _element.elements[i].display.render = element.elements[i].display.render; + } + if (_element.elements[i].type === "select" && typeof element.elements[i].allowedValues === "function") { + _element.elements[i].allowedValues = element.elements[i].allowedValues; + } + if (typeof element.elements[i]?.validation?.validate === "function") { + _element.elements[i].validation.validate = element.elements[i].validation.validate; + } + if (typeof element.elements[i]?.save === "function") { + _element.elements[i].save = element.elements[i].save; + } + // if (typeof element.elements[i]?.validation?.message === "function") { + // _element.elements[i].validation.message = element.elements[i].validation.message; + // } + // Copy JSON stringify and parse ignores functions, we need to copy them + if (typeof element.elements[i]?.display?.disabled === "function") { + _element.elements[i].display.disabled = element.elements[i].display.disabled; + } + if (typeof element.elements[i]?.display?.visible === "function") { + _element.elements[i].display.visible = element.elements[i].display.visible; + } + } + return html`
${element.display.view(item)} @@ -1399,7 +1401,7 @@ export default class DataForm extends LitElement {
`; - }) + }) }
@@ -1875,16 +1877,16 @@ export default class DataForm extends LitElement {
${buttonsVisible && buttonsLayout?.toUpperCase() === "TOP" ? this.renderButtons(null, this.activeSection) : null}
@@ -1903,15 +1905,15 @@ export default class DataForm extends LitElement {
diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 93ea14ea40..a1d3836b84 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -132,7 +132,8 @@ export default class SampleView extends LitElement { classes: "h1", }, displaySection: { - // visible: sample => sample?.id, + visible: sample => sample?.id, + showPDF: true }, elements: [ { @@ -140,13 +141,12 @@ export default class SampleView extends LitElement { type: "custom", display: { visible: sample => sample?.id, - render: data => `
${data.id} (UUID: ${data.uuid})
`, + render: data => `${data.id} (UUID: ${data.uuid})`, }, }, { title: "Individual ID", field: "individualId", - type: "label", }, { title: "Files", @@ -160,7 +160,6 @@ export default class SampleView extends LitElement { { title: "Somatic", field: "somatic", - type: "label", display: { defaultValue: "false", }, @@ -168,14 +167,13 @@ export default class SampleView extends LitElement { { title: "Version", field: "version", - type: "label", }, { title: "Status", field: "internal.status", type: "custom", display: { - render: field => `
${field?.name} (${UtilsNew.dateFormatter(field?.date)})
`, + render: field => `(${UtilsNew.dateFormatter(field?.date)})`, }, }, { @@ -183,7 +181,7 @@ export default class SampleView extends LitElement { field: "creationDate", type: "custom", display: { - render: field => `
${UtilsNew.dateFormatter(field)}
`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { @@ -191,13 +189,12 @@ export default class SampleView extends LitElement { field: "modificationDate", type: "custom", display: { - render: field => `
${UtilsNew.dateFormatter(field)}
`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { title: "Description", field: "description", - type: "label", defaultValue: "N/A", }, // { @@ -242,7 +239,8 @@ export default class SampleView extends LitElement { // italics: false // }, // content: [] - const pdfDocument = new PdfBuilder(this.sample, dataFormConfig); + const dataFormConf = this.getDefaultConfig(); + const pdfDocument = new PdfBuilder(this.sample, dataFormConf); pdfDocument.exportToPdf(); } @@ -283,6 +281,7 @@ export default class SampleView extends LitElement { title: "Search", display: { visible: sample => !sample?.id && this.search === true, + showPDF: false, }, elements: [ { @@ -315,7 +314,7 @@ export default class SampleView extends LitElement { type: "custom", display: { visible: sample => sample?.id, - render: data => html`${data.id} (UUID: ${data.uuid})`, + render: data => `${data.id} (UUID: ${data.uuid})`, }, }, { @@ -347,7 +346,7 @@ export default class SampleView extends LitElement { field: "internal.status", type: "custom", display: { - render: field => html`${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, + render: field => `${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, }, }, { @@ -355,7 +354,7 @@ export default class SampleView extends LitElement { field: "creationDate", type: "custom", display: { - render: field => html`${UtilsNew.dateFormatter(field)}`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { @@ -363,7 +362,7 @@ export default class SampleView extends LitElement { field: "modificationDate", type: "custom", display: { - render: field => html`${UtilsNew.dateFormatter(field)}`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { @@ -371,26 +370,26 @@ export default class SampleView extends LitElement { field: "description", defaultValue: "N/A", }, - { - title: "Phenotypes", - field: "phenotypes", - type: "list", - defaultValue: "N/A", - display: { - contentLayout: "bullets", - render: phenotype => { - let id = phenotype?.id; - if (phenotype?.id?.startsWith("HP:")) { - id = html` - - ${phenotype.id} - - `; - } - return phenotype?.name ? html`${phenotype.name} (${id})}` : html`${id}`; - }, - }, - }, + // { + // title: "Phenotypes", + // field: "phenotypes", + // type: "list", + // defaultValue: "N/A", + // display: { + // contentLayout: "bullets", + // render: phenotype => { + // let id = phenotype?.id; + // if (phenotype?.id?.startsWith("HP:")) { + // id = html` + // + // ${phenotype.id} + // + // `; + // } + // return phenotype?.name ? html`${phenotype.name} (${id})}` : html`${id}`; + // }, + // }, + // }, /* { title: "Annotation sets", From 3d5eff87f43950b92efcb20f6955ded66e9832ad Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:35:41 +0200 Subject: [PATCH 013/153] wc: reformat code #TASK-4860 --- src/webcomponents/commons/forms/data-form.js | 138 +++++++++---------- src/webcomponents/sample/sample-view.js | 3 +- 2 files changed, 70 insertions(+), 71 deletions(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index bb7cd5894b..25df38a65c 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -385,31 +385,31 @@ export default class DataForm extends LitElement { return html`
${this.config?.display.layout.map(section => { - const sectionClassName = section.className ?? section.classes ?? ""; - const sectionStyle = section.style ?? ""; + const sectionClassName = section.className ?? section.classes ?? ""; + const sectionStyle = section.style ?? ""; - if (section.id) { - return html` + if (section.id) { + return html`
${this._createSection(this.config.sections.find(s => s.id === section.id))}
`; - } else { - return html` + } else { + return html`
${(section.sections || []).map(subsection => { - const subsectionClassName = subsection.className ?? subsection.classes ?? ""; - const subsectionStyle = subsection.style ?? ""; - return subsection.id && html` + const subsectionClassName = subsection.className ?? subsection.classes ?? ""; + const subsectionStyle = subsection.style ?? ""; + return subsection.id && html`
${this._createSection(this.config.sections.find(s => s.id === subsection.id))}
`; - })} + })}
`; - } - })} + } + })}
`; } else { @@ -1061,32 +1061,32 @@ export default class DataForm extends LitElement { ` : null} ${array - .map(row => html` + .map(row => html` ${element.display.columns - .map(elem => { - const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; - const elemStyle = elem.display?.style ?? ""; - let content = null; - - // Check the element type - switch (elem.type) { - case "complex": - content = this._createComplexElement(elem, row); - break; - case "custom": - content = elem.display?.render && elem.display.render(this.getValue(elem.field, row)); - break; - default: - content = this.getValue(elem.field, row, elem.defaultValue, elem.format); - } - - return html` + .map(elem => { + const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; + const elemStyle = elem.display?.style ?? ""; + let content = null; + + // Check the element type + switch (elem.type) { + case "complex": + content = this._createComplexElement(elem, row); + break; + case "custom": + content = elem.display?.render && elem.display.render(this.getValue(elem.field, row)); + break; + default: + content = this.getValue(elem.field, row, elem.defaultValue, elem.format); + } + + return html` ${content} `; - })} + })} `)} @@ -1338,38 +1338,38 @@ export default class DataForm extends LitElement { const view = html`
${items?.slice(0, maxNumItems) - .map((item, index) => { - const _element = JSON.parse(JSON.stringify(element)); - // We create 'virtual' element fields: phenotypes[].1.id, by doing this all existing - // items have a virtual element associated, this will allow to get the proper value later. - for (let i = 0; i< _element.elements.length; i++) { - // This support object nested - const [left, right] = _element.elements[i].field.split("[]."); - _element.elements[i].field = left + "[]." + index + "." + right; - if (_element.elements[i].type === "custom") { - _element.elements[i].display.render = element.elements[i].display.render; - } - if (_element.elements[i].type === "select" && typeof element.elements[i].allowedValues === "function") { - _element.elements[i].allowedValues = element.elements[i].allowedValues; - } - if (typeof element.elements[i]?.validation?.validate === "function") { - _element.elements[i].validation.validate = element.elements[i].validation.validate; - } - if (typeof element.elements[i]?.save === "function") { - _element.elements[i].save = element.elements[i].save; - } - // if (typeof element.elements[i]?.validation?.message === "function") { - // _element.elements[i].validation.message = element.elements[i].validation.message; - // } - // Copy JSON stringify and parse ignores functions, we need to copy them - if (typeof element.elements[i]?.display?.disabled === "function") { - _element.elements[i].display.disabled = element.elements[i].display.disabled; - } - if (typeof element.elements[i]?.display?.visible === "function") { - _element.elements[i].display.visible = element.elements[i].display.visible; - } - } - return html` + .map((item, index) => { + const _element = JSON.parse(JSON.stringify(element)); + // We create 'virtual' element fields: phenotypes[].1.id, by doing this all existing + // items have a virtual element associated, this will allow to get the proper value later. + for (let i = 0; i< _element.elements.length; i++) { + // This support object nested + const [left, right] = _element.elements[i].field.split("[]."); + _element.elements[i].field = left + "[]." + index + "." + right; + if (_element.elements[i].type === "custom") { + _element.elements[i].display.render = element.elements[i].display.render; + } + if (_element.elements[i].type === "select" && typeof element.elements[i].allowedValues === "function") { + _element.elements[i].allowedValues = element.elements[i].allowedValues; + } + if (typeof element.elements[i]?.validation?.validate === "function") { + _element.elements[i].validation.validate = element.elements[i].validation.validate; + } + if (typeof element.elements[i]?.save === "function") { + _element.elements[i].save = element.elements[i].save; + } + // if (typeof element.elements[i]?.validation?.message === "function") { + // _element.elements[i].validation.message = element.elements[i].validation.message; + // } + // Copy JSON stringify and parse ignores functions, we need to copy them + if (typeof element.elements[i]?.display?.disabled === "function") { + _element.elements[i].display.disabled = element.elements[i].display.disabled; + } + if (typeof element.elements[i]?.display?.visible === "function") { + _element.elements[i].display.visible = element.elements[i].display.visible; + } + } + return html`
${element.display.view(item)} @@ -1401,7 +1401,7 @@ export default class DataForm extends LitElement {
`; - }) + }) }
@@ -1877,16 +1877,16 @@ export default class DataForm extends LitElement {
${buttonsVisible && buttonsLayout?.toUpperCase() === "TOP" ? this.renderButtons(null, this.activeSection) : null}
diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index a1d3836b84..181e13b6d9 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -132,8 +132,7 @@ export default class SampleView extends LitElement { classes: "h1", }, displaySection: { - visible: sample => sample?.id, - showPDF: true + // visible: sample => sample?.id, }, elements: [ { From a2522873db05cfe12c429f529ee038d485943a72 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Fri, 25 Aug 2023 18:02:06 +0200 Subject: [PATCH 014/153] wc: support showPDF for elements #TASK-4860 --- src/core/pdf-builder.js | 11 ++++--- src/webcomponents/sample/sample-view.js | 41 +++++++++++++------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 6164e84b7f..5d8d4d8b60 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -135,11 +135,12 @@ export default class PdfBuilder { } #getBooleanValue(value, defaultValue) { - let _defaultValue = typeof defaultValue !== "undefined" ? defaultValue : true; + const _defaultValue = typeof defaultValue !== "undefined" ? defaultValue : true; if (typeof value !== "undefined" && value !== null) { + if (typeof value === "boolean") { - _defaultValue = value; + return value; } if (typeof value === "function") { @@ -155,7 +156,7 @@ export default class PdfBuilder { #getVisibleSections() { return this.docDefinitionConfig.sections .filter(section => section.elements[0].type !== "notification" || section.elements.length > 1) - .filter(section => this.#getBooleanValue((section?.display?.visible) && (section?.display?.showPDF)), false); + .filter(section => this.#getBooleanValue(section?.display?.visible, true) && this.#getBooleanValue(section?.display?.showPDF, true)); } /** @@ -218,7 +219,9 @@ export default class PdfBuilder { ...titleStyleDefault, } }), - section.elements.map(element => this.#writePdfElement(element, section)) + section.elements + .filter(element => this.#getBooleanValue(element?.display?.showPDF, true)) + .map(element => this.#writePdfElement(element, section)) ], ...section?.display }; diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 181e13b6d9..10339195fa 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -369,26 +369,27 @@ export default class SampleView extends LitElement { field: "description", defaultValue: "N/A", }, - // { - // title: "Phenotypes", - // field: "phenotypes", - // type: "list", - // defaultValue: "N/A", - // display: { - // contentLayout: "bullets", - // render: phenotype => { - // let id = phenotype?.id; - // if (phenotype?.id?.startsWith("HP:")) { - // id = html` - // - // ${phenotype.id} - // - // `; - // } - // return phenotype?.name ? html`${phenotype.name} (${id})}` : html`${id}`; - // }, - // }, - // }, + { + title: "Phenotypes", + field: "phenotypes", + type: "list", + defaultValue: "N/A", + display: { + showPDF: false, + contentLayout: "bullets", + render: phenotype => { + let id = phenotype?.id; + if (phenotype?.id?.startsWith("HP:")) { + id = html` + + ${phenotype.id} + + `; + } + return phenotype?.name ? html`${phenotype.name} (${id})}` : html`${id}`; + }, + }, + }, /* { title: "Annotation sets", From 5674ab3c8cdcba774ec68275e27c8a568dd1c682 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:35:07 +0200 Subject: [PATCH 015/153] wc: support render html for listElement PDF #TASK-4860 --- src/core/pdf-builder.js | 19 ++- src/webcomponents/commons/forms/data-form.js | 4 +- src/webcomponents/sample/sample-view.js | 120 +------------------ 3 files changed, 21 insertions(+), 122 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 5d8d4d8b60..b076439d4e 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -192,7 +192,7 @@ export default class PdfBuilder { case "table": content.push(this.#createTableElement(element)); break; - case "column": // layout + case "column": content.push(this.#createColumnElement(element)); break; case "list": @@ -214,7 +214,8 @@ export default class PdfBuilder { }; return { stack: [ - this.#creatTextElement({text: section.title, + this.#creatTextElement({ + text: section.title, display: { ...titleStyleDefault, } @@ -351,14 +352,22 @@ export default class PdfBuilder { const array = this.#getValue(element.field, element?.defaultValue || []); // ol or ul const contentLayout = element.display?.contentLayout === "bullets" ? "ul" : "ol"; - // return { - // [contentLayout]: [...array] - // }; const labelStyleDefault = { propsStyle: { bold: true } }; + + if (element.display?.render) { + const title = element?.title; + const content = `
    ${array?.map(item => `
  • ${element.display.render(item)}
  • `).join("")}
`; + const htmlStyleDefault = { + ignoreStyles: ["font-family"] + }; + const container = `
${title ? title + ": " : ""}${content}
`; + return htmlToPdfmake(container, {...htmlStyleDefault}); + } + return { stack: [ this.#creatTextElement({ diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 25df38a65c..e16d62b80a 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -959,7 +959,8 @@ export default class DataForm extends LitElement { let values = []; if (element.display?.render) { for (const object of array) { - const value = element.display.render(object); + const valueHTML = element.display.render(object); + const value = typeof valueHTML === "string" ? UtilsNew.renderHTML(valueHTML) : valueHTML; values.push(value); } } else { @@ -1227,6 +1228,7 @@ export default class DataForm extends LitElement { // We also allow to call to 'onFilterChange' function. const contentHTML = element.display.render(data, value => this.onFilterChange(element, value), this.updateParams, this.data, item); // unsafeHTML or utilsNew.renderHTML + // html`` won't render html string inside literal string, so i't necessary to use renderHTML. const content = typeof contentHTML === "string" ? UtilsNew.renderHTML(contentHTML) : contentHTML; if (content) { return this._createElementTemplate(element, data, content); diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 10339195fa..7c403741ac 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -121,115 +121,6 @@ export default class SampleView extends LitElement { } onDownloadPdf() { - const dataFormConfig = { - title: "Summary", - icon: "", - display: this.displayConfig || this.displayConfigDefault, - sections: [ - { - title: "General", - display: { - classes: "h1", - }, - displaySection: { - // visible: sample => sample?.id, - }, - elements: [ - { - title: "Sample ID", - type: "custom", - display: { - visible: sample => sample?.id, - render: data => `${data.id} (UUID: ${data.uuid})`, - }, - }, - { - title: "Individual ID", - field: "individualId", - }, - { - title: "Files", - field: "fileIds", - type: "list", - display: { - defaultValue: "Files not found or empty", - contentLayout: "bullets", - }, - }, - { - title: "Somatic", - field: "somatic", - display: { - defaultValue: "false", - }, - }, - { - title: "Version", - field: "version", - }, - { - title: "Status", - field: "internal.status", - type: "custom", - display: { - render: field => `(${UtilsNew.dateFormatter(field?.date)})`, - }, - }, - { - title: "Creation Date", - field: "creationDate", - type: "custom", - display: { - render: field => `${UtilsNew.dateFormatter(field)}`, - }, - }, - { - title: "Modification Date", - field: "modificationDate", - type: "custom", - display: { - render: field => `${UtilsNew.dateFormatter(field)}`, - }, - }, - { - title: "Description", - field: "description", - defaultValue: "N/A", - }, - // { - // title: "Phenotypes", - // field: "phenotypes", - // type: "list", - // defaultValue: "N/A", - // display: { - // contentLayout: "bullets", - // render: phenotype => { - // // let id = phenotype?.id; - // // if (phenotype?.id?.startsWith("HP:")) { - // // id = html` - // // - // // ${phenotype.id} - // // - // // `; - // // } - // return phenotype?.name ? `${phenotype.name} (${id})}` : `${id}`; - // }, - // }, - // }, - /* - { - title: "Annotation sets", - field: "annotationSets", - type: "custom", - display: { - render: field => html`` - } - } - */ - ], - }, - ], - }; // watermark: { // text: "Draft Report", // color: "blue", @@ -375,18 +266,15 @@ export default class SampleView extends LitElement { type: "list", defaultValue: "N/A", display: { - showPDF: false, + // showPDF: false, contentLayout: "bullets", render: phenotype => { let id = phenotype?.id; if (phenotype?.id?.startsWith("HP:")) { - id = html` - - ${phenotype.id} - - `; + id = ` + ${phenotype.id}`; } - return phenotype?.name ? html`${phenotype.name} (${id})}` : html`${id}`; + return phenotype?.name ? `${phenotype.name} (${id})` : `${id}`; }, }, }, From cdf80def8db2a4d968ac970095cf2a3f470e91c9 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:23:54 +0200 Subject: [PATCH 016/153] wc: fix #createTableElement PDF #TASK-4860 --- src/core/pdf-builder.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index b076439d4e..84cf2457f7 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -267,7 +267,7 @@ export default class PdfBuilder { // } #createTableElement(element) { - const cols = element.display.columns; + const cols = [element.display.columns]; // Initialize headers const headIndex = []; // Store column indices that need to be skipped const headers = cols.map(columns => { @@ -281,15 +281,17 @@ export default class PdfBuilder { // Create a cell with the specified properties const cell = { - content: column.title, + text: column.title, display: { - bold: true, - colSpan: column.colspan, - rowSpan: column.rowspan + propsStyle: { + bold: true, + colSpan: column.colspan, + rowSpan: column.rowspan, + }, } }; - head.push(this.text(cell)); + head.push(this.#creatTextElement(cell)); if (column?.colspan > 1) { // Store the index of the column to skip From 0f3918678cef65d203d639d659ce9b908adf282c Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:24:42 +0200 Subject: [PATCH 017/153] wc: refactor pdf-builder #TASK-4860 --- src/core/pdf-builder.js | 93 ++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 84cf2457f7..2e5e11c884 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -3,34 +3,7 @@ import UtilsNew from "./utils-new"; export default class PdfBuilder { - // https://pdfmake.github.io/docs/0.1/document-definition-object/styling/#style-properties - /** - * @typedef {Object} Style - * @property {string} font - name of the font - * @property {number} fontSize - size of the font in pt - * @property {string[]} fontFeatures - array of advanced typographic features supported in TTF fonts (supported features depend on font file) - * @property {number} lineHeight - the line height (default: 1) - * @property {boolean} bold - whether to use bold text (default: false) - * @property {boolean} italics - whether to use italic text (default: false) - * @property {string} alignment - (‘left’ or ‘center’ or ‘right’ or ‘justify’) the alignment of the text - * @property {number} characterSpacing - size of the letter spacing in pt - * @property {string} color - the color of the text (color name e.g., ‘blue’ or hexadecimal color e.g., ‘#ff5500’) - * @property {string} background - the background color of the text - * @property {string} markerColor - the color of the bullets in a buletted list - * @property {string} decoration - the text decoration to apply (‘underline’ or ‘lineThrough’ or ‘overline’) - * @property {string} decorationStyle - the style of the text decoration (‘dashed’ or ‘dotted’ or ‘double’ or ‘wavy’) - * @property {string} decorationColor - the color of the text decoration, see color - **/ - - /** - * @param {Style} style - Define the style config you want to use for the element - * @returns {Style} return style - */ - styleObject(style) { - return {...style}; - } - - stylesDefault = { + defaultStyles = { h1: { fontSize: 24, bold: true, @@ -66,7 +39,7 @@ export default class PdfBuilder { } }; - tableLayoutDefault = { + defaultTableLayout = { headerVerticalBlueLine: { // top & bottom hLineWidth: function () { @@ -101,25 +74,25 @@ export default class PdfBuilder { this.docDefinitionConfig = dataFormConfig; this.docDefinition = { pageSize: "A4", - styles: {...this.stylesDefault, ...this.docDefinitionConfig?.styles}, + styles: {...this.defaultStyles, ...dataFormConfig?.styles}, defaultStyle: { fontSize: 10 }, - watermaker: {...this.docDefinitionConfig.watermaker}, - content: [this.docDefinitionConfig.sections ? this.#transformData() : []] + watermark: {...dataFormConfig.displayDoc.watermark}, + content: [this.#creatTextElement(dataFormConfig?.displayDoc?.headerTitle), dataFormConfig.sections ? this.#transformData() : []] }; } exportToPdf() { - pdfMake.createPdf(this.docDefinition, this.tableLayoutDefault).open(); + pdfMake.createPdf(this.docDefinition, this.defaultTableLayout).open(); } downloadPdf() { - pdfMake.createPdf(this.doc).download(); + pdfMake.createPdf(this.docDefinition).download(); } print() { - pdfMake.createPdf(this.doc).print(); + pdfMake.createPdf(this.docDefinition).print(); } pdfBlob() { @@ -155,7 +128,7 @@ export default class PdfBuilder { #getVisibleSections() { return this.docDefinitionConfig.sections - .filter(section => section.elements[0].type !== "notification" || section.elements.length > 1) + .filter(section => section?.elements[0]?.type !== "notification" || section?.elements?.length > 1) .filter(section => this.#getBooleanValue(section?.display?.visible, true) && this.#getBooleanValue(section?.display?.showPDF, true)); } @@ -209,7 +182,7 @@ export default class PdfBuilder { } #writePdfSection(section) { - const titleStyleDefault = { + const defaultTitleStyle = { classes: "h1" }; return { @@ -217,7 +190,7 @@ export default class PdfBuilder { this.#creatTextElement({ text: section.title, display: { - ...titleStyleDefault, + ...defaultTitleStyle, } }), section.elements @@ -229,7 +202,7 @@ export default class PdfBuilder { } #creatTextElement(element) { - const value = element?.text || ""; + const value = element?.text || element?.title || ""; const classes = element.display?.classes || {}; const propsStyle = element.display?.propsStyle || {}; return { @@ -240,7 +213,7 @@ export default class PdfBuilder { } #createLabelElement(element) { - const labelStyleDefault = { + const defaultLabelStyle = { propsStyle: { bold: true } @@ -251,7 +224,7 @@ export default class PdfBuilder { this.#creatTextElement({ text: `${element?.title}: `, display: { - ...labelStyleDefault + ...defaultLabelStyle }}), this.#creatTextElement({text: this.#getValue(element.field, element?.defaultValue || "")}) ] @@ -354,7 +327,7 @@ export default class PdfBuilder { const array = this.#getValue(element.field, element?.defaultValue || []); // ol or ul const contentLayout = element.display?.contentLayout === "bullets" ? "ul" : "ol"; - const labelStyleDefault = { + const defaultLabelStyle = { propsStyle: { bold: true } @@ -363,11 +336,11 @@ export default class PdfBuilder { if (element.display?.render) { const title = element?.title; const content = `
    ${array?.map(item => `
  • ${element.display.render(item)}
  • `).join("")}
`; - const htmlStyleDefault = { + const defaultHtmlStyle = { ignoreStyles: ["font-family"] }; const container = `
${title ? title + ": " : ""}${content}
`; - return htmlToPdfmake(container, {...htmlStyleDefault}); + return htmlToPdfmake(container, {...defaultHtmlStyle}); } return { @@ -375,7 +348,7 @@ export default class PdfBuilder { this.#creatTextElement({ text: `${element?.title}`, display: { - ...labelStyleDefault + ...defaultLabelStyle }}), {[contentLayout]: [...array]} ] @@ -386,13 +359,39 @@ export default class PdfBuilder { const data = element?.field ? this.#getValue(element?.field, {}) : this.data; const title = element?.title; const content = element.display?.render ? element.display?.render(data) : {}; - const htmlStyleDefault = { + const defaultHtmlStyle = { removeExtraBlanks: true, ignoreStyles: ["font-family"] }; const container = `
${title ? title + ": " : ""}${content}
`; - return htmlToPdfmake(container, {...htmlStyleDefault}); + return htmlToPdfmake(container, {...defaultHtmlStyle}); } } + +// https://pdfmake.github.io/docs/0.1/document-definition-object/styling/#style-properties +/** + * @typedef {Object} Style + * @property {string} font - name of the font + * @property {number} fontSize - size of the font in pt + * @property {string[]} fontFeatures - array of advanced typographic features supported in TTF fonts (supported features depend on font file) + * @property {number} lineHeight - the line height (default: 1) + * @property {boolean} bold - whether to use bold text (default: false) + * @property {boolean} italics - whether to use italic text (default: false) + * @property {string} alignment - (‘left’ or ‘center’ or ‘right’ or ‘justify’) the alignment of the text + * @property {number} characterSpacing - size of the letter spacing in pt + * @property {string} color - the color of the text (color name e.g., ‘blue’ or hexadecimal color e.g., ‘#ff5500’) + * @property {string} background - the background color of the text + * @property {string} markerColor - the color of the bullets in a buletted list + * @property {string} decoration - the text decoration to apply (‘underline’ or ‘lineThrough’ or ‘overline’) + * @property {string} decorationStyle - the style of the text decoration (‘dashed’ or ‘dotted’ or ‘double’ or ‘wavy’) + * @property {string} decorationColor - the color of the text decoration, see color + **/ +/** + * @param {Style} style - Define the style config you want to use for the element + * @returns {Style} return style +*/ +export function stylePdf(style) { + return {...style}; +} From 41d4da0bc5925233af42976a3982e863d1ddeef1 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:25:36 +0200 Subject: [PATCH 018/153] wc: add title & watermark to sample-view pdf #TASK-4860 --- src/webcomponents/sample/sample-view.js | 32 +++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 7c403741ac..ac0fc214c4 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -23,8 +23,7 @@ import "../commons/forms/data-form.js"; import "../commons/filters/catalog-search-autocomplete.js"; import "../study/annotationset/annotation-set-view.js"; import "../loading-spinner.js"; -import PdfBuilder from "../../core/pdf-builder.js"; - +import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; export default class SampleView extends LitElement { constructor() { @@ -121,14 +120,6 @@ export default class SampleView extends LitElement { } onDownloadPdf() { - // watermark: { - // text: "Draft Report", - // color: "blue", - // opacity: 0.3, - // bold: true, - // italics: false - // }, - // content: [] const dataFormConf = this.getDefaultConfig(); const pdfDocument = new PdfBuilder(this.sample, dataFormConf); pdfDocument.exportToPdf(); @@ -166,6 +157,27 @@ export default class SampleView extends LitElement { title: "Summary", icon: "", display: this.displayConfig || this.displayConfigDefault, + displayDoc: { + headerTitle: { + title: `Sample ${this.sample?.id}`, + display: { + classes: "h1", + propsStyle: { + ...stylePdf({ + alignment: "center", + bold: true, + }) + }, + }, + }, + watermark: { + text: "Demo", + color: "blue", + opacity: 0.3, + bold: true, + italics: false + }, + }, sections: [ { title: "Search", From c9c3d50fde4c81496cee5851274b015204fd292b Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:26:24 +0200 Subject: [PATCH 019/153] wc: add export pdf for cohort-view #TASK-4860 --- src/webcomponents/cohort/cohort-view.js | 54 +++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index e362c28035..be9261d03e 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -18,6 +18,7 @@ import {LitElement, html} from "lit"; import UtilsNew from "../../core/utils-new.js"; import LitUtils from "../commons/utils/lit-utils.js"; import Types from "../commons/types.js"; +import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; import "../commons/forms/data-form.js"; import "../loading-spinner.js"; import "../study/annotationset/annotation-set-view.js"; @@ -117,6 +118,13 @@ export default class CohortView extends LitElement { this.cohortId = e.detail.value; } + onDownloadPdf() { + const dataFormConf = this.getDefaultConfig(); + const pdfDocument = new PdfBuilder(this.cohort, dataFormConf); + pdfDocument.exportToPdf(); + } + + render() { if (this.isLoading) { return html``; @@ -132,6 +140,11 @@ export default class CohortView extends LitElement { } return html` + @@ -144,11 +157,33 @@ export default class CohortView extends LitElement { title: "Summary", icon: "", display: this.displayConfig || this.displayConfigDefault, + displayDoc: { + headerTitle: { + title: `Cohort ${this.cohort?.id}`, + display: { + classes: "h1", + propsStyle: { + ...stylePdf({ + alignment: "center", + bold: true, + }) + }, + }, + }, + watermark: { + text: "Demo", + color: "blue", + opacity: 0.3, + bold: true, + italics: false + }, + }, sections: [ { title: "Search", display: { visible: cohort => !cohort?.id && this.search === true, + showPDF: false, }, elements: [ { @@ -218,6 +253,7 @@ export default class CohortView extends LitElement { type: "custom", defaultValue: "N/A", display: { + showPDF: false, render: field => html` @@ -240,23 +276,27 @@ export default class CohortView extends LitElement { display: { columns: [ { + id: "sample", title: "Samples ID", field: "id", + sortable: true, }, { + id: "somatic", title: "Somatic", field: "somatic", - defaultValue: "false", + sortable: true, + formatter: value => value ? "true" : "false", }, { + id: "phenotypes", title: "Phenotypes", field: "phenotypes", - type: "custom", - defaultValue: "-", - display: { - render: data => data?.length ? html`${data.map(d => d.id).join(", ")}` : "-", - }, - }, + sortable: true, + formatter: (value, row) => { + return row?.phenotypes?.length > 0 ? row.phenotypes.map(d => d.id).join(", ") : "-"; + } + } ], defaultValue: "No phenotypes found", }, From 0b37d0d7d1ff83d930b8117970c27a7de33da46f Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Mon, 28 Aug 2023 22:05:29 +0200 Subject: [PATCH 020/153] wc: Add 'widths' column for the PDF and remove the HTML custom element in the cohort view #TASK-4860 --- src/core/pdf-builder.js | 2 +- src/webcomponents/cohort/cohort-view.js | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index 2e5e11c884..b5419df4f0 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -307,9 +307,9 @@ export default class PdfBuilder { // Merge headers and rows const contents = [...headers, ...rows]; - return { table: { + widths: cols[0].map(col => col.width), // TODO modify to support nested for nested column soon... headerRows: cols.length, body: contents, }, diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index be9261d03e..6228703ea9 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -228,7 +228,7 @@ export default class CohortView extends LitElement { field: "internal.status", type: "custom", display: { - render: field => html`${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, + render: field => `${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, }, }, { @@ -236,7 +236,7 @@ export default class CohortView extends LitElement { field: "creationDate", type: "custom", display: { - render: field => html`${UtilsNew.dateFormatter(field)}`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { @@ -244,7 +244,7 @@ export default class CohortView extends LitElement { field: "modificationDate", type: "custom", display: { - render: field => html`${UtilsNew.dateFormatter(field)}`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { @@ -279,6 +279,7 @@ export default class CohortView extends LitElement { id: "sample", title: "Samples ID", field: "id", + width: "*", sortable: true, }, { @@ -286,6 +287,7 @@ export default class CohortView extends LitElement { title: "Somatic", field: "somatic", sortable: true, + width: "*", formatter: value => value ? "true" : "false", }, { @@ -293,6 +295,7 @@ export default class CohortView extends LitElement { title: "Phenotypes", field: "phenotypes", sortable: true, + width: "*", formatter: (value, row) => { return row?.phenotypes?.length > 0 ? row.phenotypes.map(d => d.id).join(", ") : "-"; } From 7b6263d6fa985cd70991e74e4274a899b6a6e125 Mon Sep 17 00:00:00 2001 From: Rodiel Martinez <10471758+Rodielm@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:07:46 +0200 Subject: [PATCH 021/153] wc: fix typo pdf #TASK-4860 --- src/core/pdf-builder.js | 4 ++-- .../clinical/clinical-analysis-review.js | 16 ++++++++-------- src/webcomponents/individual/individual-view.js | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core/pdf-builder.js b/src/core/pdf-builder.js index b5419df4f0..ad8a213092 100644 --- a/src/core/pdf-builder.js +++ b/src/core/pdf-builder.js @@ -78,8 +78,8 @@ export default class PdfBuilder { defaultStyle: { fontSize: 10 }, - watermark: {...dataFormConfig.displayDoc.watermark}, - content: [this.#creatTextElement(dataFormConfig?.displayDoc?.headerTitle), dataFormConfig.sections ? this.#transformData() : []] + watermark: {...dataFormConfig?.displayDoc.watermark}, + content: [this.#creatTextElement(dataFormConfig?.displayDoc?.headerTitle), dataFormConfig?.sections ? this.#transformData() : []] }; } diff --git a/src/webcomponents/clinical/clinical-analysis-review.js b/src/webcomponents/clinical/clinical-analysis-review.js index 2180fa5bf2..37e57eb5a7 100644 --- a/src/webcomponents/clinical/clinical-analysis-review.js +++ b/src/webcomponents/clinical/clinical-analysis-review.js @@ -21,7 +21,7 @@ import LitUtils from "../commons/utils/lit-utils.js"; import ClinicalAnalysisManager from "./clinical-analysis-manager.js"; import FormUtils from "../commons/forms/form-utils.js"; import NotificationUtils from "../commons/utils/notification-utils.js"; -import {createPdf} from "../../core/pdf-builder.js"; +import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; import "./clinical-analysis-summary.js"; import "../variant/interpretation/variant-interpreter-grid.js"; import "../disease-panel/disease-panel-grid.js"; @@ -334,13 +334,13 @@ export default class ClinicalAnalysisReview extends LitElement { } onDownloadPdf() { - const pdfDocument = createPdf({ - content: [ - "First paragraph", - "Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines" - ] - }); - pdfDocument.open(); + // const pdfDocument = new PdfBuilder({}, { + // content: [ + // "First paragraph", + // "Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines" + // ] + // }); + // pdfDocument.exportToPdf(); } render() { diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index 9d8c646813..cb4398bc76 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -184,7 +184,7 @@ export default class IndividualView extends LitElement { title: "Individual ID", type: "custom", display: { - render: data => html` + render: data => ` ${data.id} (UUID: ${data.uuid}) `, }, @@ -272,7 +272,7 @@ export default class IndividualView extends LitElement { field: "internal.status", type: "custom", display: { - render: field => field ? html`${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-" + render: field => field ? `${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-" }, }, { @@ -280,7 +280,7 @@ export default class IndividualView extends LitElement { field: "creationDate", type: "custom", display: { - render: field => field ? html`${UtilsNew.dateFormatter(field)}` : "-" + render: field => field ? UtilsNew.dateFormatter(field) : "-" }, }, { @@ -288,7 +288,7 @@ export default class IndividualView extends LitElement { field: "modificationDate", type: "custom", display: { - render: field => field ? html`${UtilsNew.dateFormatter(field)}` : "-" + render: field => field ? UtilsNew.dateFormatter(field) : "-" }, }, { From a4ed320ecc3cba9395696c45539175b58b99cf75 Mon Sep 17 00:00:00 2001 From: imedina Date: Sun, 5 Nov 2023 00:52:16 +0000 Subject: [PATCH 022/153] Different improvements implemented: - restore 'table' implementation - fix and extend 'list' functionality - extend 'complex' and 'basic' functionality with new styles - several minor fixes and code cleaning --- src/webcomponents/commons/forms/data-form.js | 371 +++++++++++++------ 1 file changed, 253 insertions(+), 118 deletions(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 2dc3b08af8..4196beeedc 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -103,7 +103,7 @@ export default class DataForm extends LitElement { this.data = this.data ?? {}; } - getValue(field, object, defaultValue, format) { + getValue(field, object, defaultValue, display) { let value = object; if (field) { const _object = object ? object : this.data; @@ -130,17 +130,24 @@ export default class DataForm extends LitElement { value = field.split(".").reduce((res, prop) => res?.[prop], _object); } - // needed for handling falsy values + // Check if 'format' exists + if (value && display?.format) { + // check if response is actually an HTML + value = UtilsNew.renderHTML(display.format(value)); + } + + // Needed for handling falsy values if (value !== undefined && value !== "") { - if (format) { - if (format.classes || format.style) { - value = html`${value}`; + if (display) { + if (display.classes || display.style) { + const style = this._parseStyleField(display.style); + value = html`${value}`; } - if (format.link) { - value = html`${value}`; + if (display.link) { + value = html`${value}`; } - if (format.decimals && !isNaN(value)) { - value = value.toFixed(format.decimals); + if (display.decimals && !isNaN(value)) { + value = value.toFixed(display.decimals); } } } else { @@ -152,20 +159,59 @@ export default class DataForm extends LitElement { return value; } - applyTemplate(template, object, matches, defaultValue) { - if (!matches) { - // eslint-disable-next-line no-param-reassign - matches = template.match(/\$\{[a-zA-Z_.\[\]]+\}/g).map(elem => elem.substring(2, elem.length - 1)); - } + applyTemplate(template, object, defaultValue, element) { + // Parse template string and find matches groups + const matches = template + .match(/\$\{[a-zA-Z_.\[\]]+\}/g) + .map(elem => elem.substring(2, elem.length - 1)); + for (const match of matches) { - const v = this.getValue(match, object, defaultValue); + let value = this.getValue(match, object, defaultValue); + // Check if 'style' has been defined for this match variable, example: + // { + // title: "Format", + // type: "complex", + // display: { + // template: "${format} (${bioformat})", + // format: { + // "format": value => value.toUpperCase() + // }, + // style: { + // format: { + // "font-weight": "bold", + // "color": "red" + // }, + // bioformat: { + // "color": "green" + // } + // } + // }, + // }, + if (element?.display?.format?.[match]) { + value = element?.display?.format?.[match](value); + } + if (element?.display?.style?.[match]) { + const style = this._parseStyleField(element?.display?.style?.[match]); + value = `${value}`; + } // eslint-disable-next-line no-param-reassign - template = template.replace("${" + match + "}", v); + template = template.replace("${" + match + "}", value); } return template; } + // Convert a 'string' or 'object' field to the HTML style string, ie. "font-weight: bold;color:red" + _parseStyleField(elementStyle) { + let style = elementStyle || ""; + if (elementStyle && typeof elementStyle === "object") { + style = Object.entries(elementStyle) + .map(([k, v]) => `${k}: ${v}`) + .join(";"); + } + return style; + } + _getType() { // In case that we are using the deprecated form type, get type from display.mode.type return (this.config.type === "form") ? this.config.display?.mode?.type ?? "" : this.config.type ?? ""; @@ -183,9 +229,9 @@ export default class DataForm extends LitElement { return layout || "bottom"; } - _getDefaultValue(element) { - // WARNING: element.defaultValue is deprecated, use element.display.defaultValue - return element?.display?.defaultValue ?? element?.defaultValue ?? this.config?.display?.defaultValue ?? ""; + _getDefaultValue(element, section) { + // Preference order: element, section and then global config + return element?.display?.defaultValue ?? section?.display?.defaultValue ?? this.config?.display?.defaultValue ?? ""; } _getErrorMessage(element, section) { @@ -494,8 +540,7 @@ export default class DataForm extends LitElement { // if not 'type' is defined we assumed is 'basic' and therefore field exist if (!element.type || element.type === "basic") { - const format = element.display?.format ?? element.display; // 'format' is the old way, to be removed - content = html`${this.getValue(element.field, this.data, this._getDefaultValue(element), format)}`; + content = html`${this.getValue(element.field, this.data, this._getDefaultValue(element, section), element.display)}`; } else { // Other 'type' are rendered by specific functions switch (element.type) { @@ -505,19 +550,19 @@ export default class DataForm extends LitElement { content = this._createTextElement(element); break; case "input-text": - content = this._createInputElement(element, "text"); + content = this._createInputElement(element, "text", section); break; case "input-num": - content = this._createInputElement(element, "number"); + content = this._createInputElement(element, "number", section); break; case "input-password": - content = this._createInputElement(element, "password"); + content = this._createInputElement(element, "password", section); break; case "input-number": - content = this._createInputNumberElement(element); + content = this._createInputNumberElement(element, section); break; case "input-date": - content = this._createInputDateElement(element); + content = this._createInputDateElement(element, section); break; case "checkbox": content = this._createCheckboxElement(element); @@ -532,13 +577,16 @@ export default class DataForm extends LitElement { content = this._createInputSelectElement(element); break; case "complex": - content = this._createComplexElement(element); + content = this._createComplexElement(element, this.data, section); break; case "list": - content = this._createListElement(element); + content = this._createListElement(element, this.data, section); break; case "table": - content = this._createTableElement(element); + content = this._createTableElement(element, section); + break; + case "image": + content = this._createImageElement(element); break; case "chart": case "plot": @@ -656,10 +704,20 @@ export default class DataForm extends LitElement { `; } + // Check if 'content' is passed as a string or an array, then we must convert it to HTML + let contentHtml = content; + if (typeof content === "string") { + contentHtml = UtilsNew.renderHTML(content); + } else { + if (Array.isArray(content)) { + contentHtml = UtilsNew.renderHTML(content.join("")); + } + } + return html`
- ${content} + ${contentHtml}
${helpMessage && helpMode !== "block" ? html`
${helpMessage}
@@ -689,7 +747,7 @@ export default class DataForm extends LitElement { ${element.display?.icon ? html` ` : null} - ${UtilsNew.renderHTML(value || "")} + ${value || ""}
`; @@ -697,8 +755,8 @@ export default class DataForm extends LitElement { } // Josemi 20220202 NOTE: this function was prev called _createInputTextElement - _createInputElement(element, type) { - const value = this.getValue(element.field) || this._getDefaultValue(element); + _createInputElement(element, type, section) { + const value = this.getValue(element.field) || this._getDefaultValue(element, section); const disabled = this._getBooleanValue(element.display?.disabled, false, element); const [min = undefined, max = undefined] = element.allowedValues || []; const step = element.step || "1"; @@ -723,8 +781,8 @@ export default class DataForm extends LitElement { return this._createElementTemplate(element, value, content); } - _createInputNumberElement(element) { - const value = this.getValue(element.field) ?? this._getDefaultValue(element); + _createInputNumberElement(element, section) { + const value = this.getValue(element.field) ?? this._getDefaultValue(element, section); const disabled = this._getBooleanValue(element?.display?.disabled, false, element); const [min = "", max = ""] = element.allowedValues || []; const step = element.step || "1"; @@ -747,8 +805,8 @@ export default class DataForm extends LitElement { return this._createElementTemplate(element, value, content); } - _createInputDateElement(element) { - const value = this.getValue(element.field) || this._getDefaultValue(element); + _createInputDateElement(element, section) { + const value = this.getValue(element.field) || this._getDefaultValue(element, section); const disabled = this._getBooleanValue(element.display?.disabled, false, element); const parseInputDate = e => { // Date returned by is in YYYY-MM-DD format, but we need YYYYMMDDHHmmss format @@ -768,7 +826,7 @@ export default class DataForm extends LitElement { } _createCheckboxElement(element) { - let value = this.getValue(element.field); // || this._getDefaultValue(element); + let value = this.getValue(element.field); const disabled = this._getBooleanValue(element.display?.disabled, false, element); // TODO to be fixed. @@ -800,7 +858,7 @@ export default class DataForm extends LitElement { * @private */ _createToggleSwitchElement(element) { - const value = this.getValue(element.field); // || this._getDefaultValue(element); + const value = this.getValue(element.field); const disabled = this._getBooleanValue(element.display?.disabled, false, element); const activeClassName = element.display?.activeClassName ?? element.display?.activeClass ?? ""; const inactiveClassName = element.display?.inactiveClassName ?? element.display?.inactiveClass ?? ""; @@ -830,7 +888,7 @@ export default class DataForm extends LitElement { } _createToggleButtonsElement(element) { - const value = this.getValue(element.field) || this._getDefaultValue(element); + const value = this.getValue(element.field); const names = element.allowedValues; const activeClassName = element.display?.activeClassName ?? element.display?.activeClass ?? ""; const inactiveClassName = element.display?.inactiveClassName ?? element.display?.inactiveClass ?? ""; @@ -939,110 +997,118 @@ export default class DataForm extends LitElement { return this._createElementTemplate(element, null, content); } - _createComplexElement(element, data = this.data) { + _createComplexElement(element, data = this.data, section) { if (!element.display?.template) { - return html`No template provided`; + return this._createElementTemplate(element, null, null, { + message: "No template provided", + className: "text-danger" + }); } + const content = html` - ${UtilsNew.renderHTML(this.applyTemplate(element.display.template, data, null, this._getDefaultValue(element)))} + ${UtilsNew.renderHTML(this.applyTemplate(element.display.template, data, this._getDefaultValue(element, section), element))} `; return this._createElementTemplate(element, null, content); } - _createListElement(element) { + _createListElement(element, data = this.data, section) { // Get values - const array = this.getValue(element.field); - const contentLayout = element.display?.contentLayout || "horizontal"; + let array = this.getValue(element.field, data); + const contentLayout = element.display?.contentLayout || "vertical"; - // Check values - if (!array || !array.length) { - const message = this._getDefaultValue(element); - return this._createElementTemplate(element, null, null, { - message: message, - }); - } + // 1. Check array and layout exist if (!Array.isArray(array)) { - const message = `Field '${element.field}' is not an array`; return this._createElementTemplate(element, null, null, { - message: message, - classname: "text-danger" + message: `Field '${element.field}' is not an array`, + className: "text-danger" }); } if (contentLayout !== "horizontal" && contentLayout !== "vertical" && contentLayout !== "bullets") { - const message = "Content layout must be 'horizontal', 'vertical' or 'bullets'"; return this._createElementTemplate(element, null, null, { - message: message, + message: "Content layout must be 'horizontal', 'vertical' or 'bullets'", className: "text-danger" }); } + // 2. Apply 'filter' and 'transform' functions if defined + if (typeof element.display?.filter === "function") { + array = element.display.filter(array); + } + if (typeof element.display?.transform === "function") { + array = element.display.transform(array); + } + + // 3. Check length of the array. This MUST be done after filtering + if (array.length === 0) { + // If empty we just print the defaultValue, this is not an error + return this._createElementTemplate(element, null, null, { + message: this._getDefaultValue(element, section), + }); + } + + // 4. Initialise values with array, this is valid for scalars, or when 'template' and 'format' do not exist // Apply the template to all Array elements and store them in 'values' - let values = []; - if (element.display?.render) { - for (const object of array) { - const valueHTML = element.display.render(object); - const value = typeof valueHTML === "string" ? UtilsNew.renderHTML(valueHTML) : valueHTML; - values.push(value); + let values = array; + if (element.display?.format || element.display?.render) { + // NOTE: 'element.display.render' is now deprecated, use 'format' instead + if (element.display?.format) { + values = array.map(item => element.display.format(item)); + } else { + values = array.map(item => element.display.render(item)); } } else { if (element.display?.template) { - const matches = element.display.template.match(/\$\{[a-zA-Z_.\[\]]+\}/g).map(elem => elem.substring(2, elem.length - 1)); - for (const object of array) { - const value = this.applyTemplate(element.display.template, object, matches, this._getDefaultValue(element)); - values.push(value); - } - } else { - // if 'display.template' does not exist means it is an array of scalars - values = array; + values = array + .map(item => this.applyTemplate(element.display.template, item, this._getDefaultValue(element, section), element)); } } - // Render element values - let content = "-"; + // 5. Render element values + let content = this._getDefaultValue(element, section); switch (contentLayout) { case "horizontal": const separator = element?.display?.separator ?? ", "; - content = html` - ${values.map(elem => html` - ${elem} - ${separator ? html`${separator}` : ""} - `)} - `; + content = ` + ${values + .map(elem => `${elem}`) + .join(`${separator}`) + }`; break; case "vertical": - content = html` - ${values.map(elem => html` -
${elem}
- `)} - `; + content = ` + ${values + .map(elem => `
${elem}
`) + .join("") + }`; break; case "bullets": - content = html` + content = `
    - ${values.map(elem => html` -
  • ${elem}
  • - `)} + ${values.map(elem => `
  • ${elem}
  • `).join("")}
`; break; } - return this._createElementTemplate(element, null, content); + return this._createElementTemplate(element, null, content); } - _createTableElement(element) { - // Get values + + _createTableElement(element, section) { + // Get valid parameters let array = this.getValue(element.field, null, element.defaultValue); + const tableClassName = element.display?.className || ""; + const tableStyle = this._parseStyleField(element.display?.style) || ""; + const headerClassName = element.display?.headerClassName || ""; + const headerStyle = this._parseStyleField(element.display?.headerStyle) || ""; + const headerVisible = this._getBooleanValue(element.display?.headerVisible, true); const errorMessage = this._getErrorMessage(element); const errorClassName = element.display?.errorClassName ?? element.display?.errorClasses ?? "text-danger"; - const headerVisible = this._getBooleanValue(element.display?.headerVisible, true); - const tableClassName = element.display?.className || ""; - const tableStyle = element.display?.style || ""; - // Check values + // 1. Check field exists, and it is an array. Also, check 'columns' is defined if (!array) { const message = errorMessage ?? `Type 'table' requires a valid array field: ${element.field} not found`; return this._createElementTemplate(element, null, null, { @@ -1057,37 +1123,105 @@ export default class DataForm extends LitElement { className: errorClassName, }); } - if (typeof element.display?.transform === "function") { - array = element.display.transform(array); - } - if (!array.length) { - const message = this._getDefaultValue(element); + if (!element.display && !element.display.columns) { + const message = "Type 'table' requires a 'columns' array"; return this._createElementTemplate(element, null, null, { message: message, + className: errorClassName, }); } - if (!element.display && !element.display.columns) { - const message = "Type 'table' requires a 'columns' array"; + + // 2. Apply 'filter' and 'transform' functions if defined + if (typeof element.display?.filter === "function") { + array = element.display.filter(array); + } + if (typeof element.display?.transform === "function") { + array = element.display.transform(array); + } + + // 3. Check length of the array. This MUST be done after filtering + if (!array.length) { + const message = this._getDefaultValue(element, section); return this._createElementTemplate(element, null, null, { message: message, - className: errorClassName, }); } - const config = { - pagination: element.display?.pagination ?? false, - search: element.display?.search ?? false, - searchAlign: element.display?.searchAlign ?? "right", - showHeader: element.display?.showHeader ?? true, - }; + const content = html` + + ${headerVisible ? html` + + + ${element.display.columns.map(elem => html` + + `)} + + ` : nothing} + + ${array + .map(row => html` + + ${element.display.columns + .map(elem => { + const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; + const elemStyle = this._parseStyleField(elem.display?.style); + + // Check the element type + let content; + switch (elem.type) { + case "complex": + content = this._createComplexElement(elem, row); + break; + case "list": + content = this._createListElement(elem, row, section); + break; + case "image": + content = this._createImageElement(elem); + break; + case "custom": + content = elem.display?.render(this.getValue(elem.field, row)); + break; + default: + content = this.getValue(elem.field, row, elem.defaultValue, elem.format ?? elem.display); + } - const content = html ` - - + return html` + + `; + })} + + `)} + +
${elem.title || elem.name}
+ ${content} +
+ `; + + // const config = { + // pagination: element.display?.pagination ?? false, + // search: element.display?.search ?? false, + // searchAlign: element.display?.searchAlign ?? "right", + // showHeader: element.display?.showHeader ?? true, + // }; + // + // const content = html ` + // + // + // `; + return this._createElementTemplate(element, null, content); + } + + _createImageElement(element) { + const value = (element.display?.image) ? element.display.image(this.data) : this.getValue(element.field); + const content = html` + + `; + return this._createElementTemplate(element, null, content); } @@ -1151,6 +1285,7 @@ export default class DataForm extends LitElement { .data="${json}"> ` : content = this._getDefaultValue(element); + return this._createElementTemplate(element, null, content); } @@ -1230,10 +1365,10 @@ export default class DataForm extends LitElement { // Call to render function, it must be defined! // We also allow to call to 'onFilterChange' function. - const contentHTML = element.display.render(data, value => this.onFilterChange(element, value), this.updateParams, this.data, item); + const content = element.display.render(data, value => this.onFilterChange(element, value), this.updateParams, this.data, item); // unsafeHTML or utilsNew.renderHTML // html`` won't render html string inside literal string, so i't necessary to use renderHTML. - const content = typeof contentHTML === "string" ? UtilsNew.renderHTML(contentHTML) : contentHTML; + // const content = typeof contentHTML === "string" ? UtilsNew.renderHTML(contentHTML) : contentHTML; if (content) { return this._createElementTemplate(element, data, content); } else { From 49ee91898b65c97fb6d220553777459e7d022fc6 Mon Sep 17 00:00:00 2001 From: imedina Date: Sun, 5 Nov 2023 01:04:09 +0000 Subject: [PATCH 023/153] Update views to new data-form improvements --- src/sites/iva/conf/config.js | 6 +- src/webcomponents/cohort/cohort-view.js | 16 +-- src/webcomponents/family/family-view.js | 80 +++++++----- src/webcomponents/file/file-view.js | 16 ++- .../individual/individual-view.js | 117 +++++++++++++----- src/webcomponents/job/job-view.js | 70 ++++++----- 6 files changed, 190 insertions(+), 115 deletions(-) diff --git a/src/sites/iva/conf/config.js b/src/sites/iva/conf/config.js index 6c8636bd0e..b499608353 100644 --- a/src/sites/iva/conf/config.js +++ b/src/sites/iva/conf/config.js @@ -27,15 +27,15 @@ const hosts = [ }, { id: "testteam", - url: "https://test.app.zettagenomics.com/testteam/opencga" + url: "https://demo.app.zettagenomics.com/opencga" }, ]; const opencga = { - host: hosts[1].url, + host: hosts[2].url, version: "v2", cookie: { - prefix: "iva-" + hosts[1].id + prefix: "iva-" + hosts[2].id }, sso: { active: false, diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index bd974e3896..49fedb715f 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -271,30 +271,30 @@ export default class CohortView extends LitElement { id: "sample", title: "Samples ID", field: "id", - width: "*", - sortable: true, + // width: "*", + // sortable: true, }, { id: "somatic", title: "Somatic", field: "somatic", - sortable: true, - width: "*", + // sortable: true, + // width: "*", formatter: value => value ? "true" : "false", }, { id: "phenotypes", title: "Phenotypes", field: "phenotypes", - sortable: true, - width: "*", + // sortable: true, + // width: "*", formatter: (value, row) => { return row?.phenotypes?.length > 0 ? row.phenotypes.map(d => d.id).join(", ") : "-"; } } ], - pagination: true, - search: true, + // pagination: true, + // search: true, }, } ], diff --git a/src/webcomponents/family/family-view.js b/src/webcomponents/family/family-view.js index 274a8ae29d..1794b78515 100644 --- a/src/webcomponents/family/family-view.js +++ b/src/webcomponents/family/family-view.js @@ -234,7 +234,7 @@ export default class FamilyView extends LitElement { title: "Family ID", type: "custom", display: { - render: data => html`${data.id} (UUID: ${data.uuid})`, + render: data => `${data.id} (UUID: ${data.uuid})`, } }, { @@ -247,7 +247,7 @@ export default class FamilyView extends LitElement { type: "list", display: { contentLayout: "vertical", - render: disorder => UtilsNew.renderHTML(CatalogGridFormatter.disorderFormatter(disorder)), + render: disorder => CatalogGridFormatter.disorderFormatter(disorder), defaultValue: "N/A" } }, @@ -256,14 +256,15 @@ export default class FamilyView extends LitElement { field: "phenotypes", type: "list", display: { - visible: !this._config?.hiddenFields?.includes("phenotypes"), - contentLayout: "bullets", - render: phenotype => { + visible: !this.settings?.hiddenFields?.includes("phenotypes"), + contentLayout: "vertical", + // render: phenotype => { + format: phenotype => { let id = phenotype.id; if (phenotype.id.startsWith("HP:")) { - id = html`${phenotype.id}`; + id = `${phenotype.id}`; } - return html`${phenotype.name} (${id})`; + return `${phenotype.name} (${id})`; }, defaultValue: "N/A" } @@ -275,10 +276,11 @@ export default class FamilyView extends LitElement { { title: "Creation Date", field: "creationDate", - type: "custom", + // type: "custom", display: { - visible: !this._config?.hiddenFields?.includes("creationDate"), - render: field => html`${UtilsNew.dateFormatter(field)}`, + visible: !this.settings?.hiddenFields?.includes("creationDate"), + // render: field => `${UtilsNew.dateFormatter(field)}`, + format: field => `${UtilsNew.dateFormatter(field)}`, } }, { @@ -301,55 +303,77 @@ export default class FamilyView extends LitElement { field: "members", type: "table", display: { - layout: "horizontal", + // layout: "horizontal", + defaultValue: "-", columns: [ { title: "Individual ID", - field: "id" + field: "id", + display: { + style: { + "font-weight": "bold" + } + } }, { title: "Sex", field: "sex", - formatter: value => value?.id || value || "Not specified", + // formatter: value => value?.id || value || "Not specified", + display: { + format: sex => sex.id + } }, { title: "Father ID", field: "father.id", - formatter: value => value ?? "-" + // formatter: value => value ?? "-" }, { title: "Mother ID", field: "mother.id", - formatter: value => value ?? "-" + // formatter: value => value ?? "-" }, { title: "Disorders", field: "disorders", - formatter: values => values?.length ? `${values.map(d => d.id).join(", ")}` : "-", + type: "list", + // formatter: values => values?.length ? `${values.map(d => d.id).join(", ")}` : "-", + display: { + defaultValue: "-", + format: disorder => CatalogGridFormatter.disorderFormatter(disorder) + } }, { title: "Phenotypes", field: "phenotypes", - formatter: values => values?.length ? `${values.map(d => d.id).join(", ")}` : "-", - }, - { - title: "Life Status", - field: "lifeStatus", + type: "list", + // formatter: values => values?.length ? `${values.map(d => d.id).join(", ")}` : "-", + display: { + format: phenotype => CatalogGridFormatter.phenotypesFormatter([phenotype]) + } } ] } }, + // { + // title: "Pedigree", + // type: "custom", + // display: { + // render: () => html` + // + // + // `, + // } + // }, { title: "Pedigree", - type: "custom", + type: "image", + field: "pedigreeGraph.base64", display: { - render: () => html` - - - `, + visible: !this.settings?.hiddenFields?.includes("pedigreeGraph.base64"), } - } + }, ] } ] diff --git a/src/webcomponents/file/file-view.js b/src/webcomponents/file/file-view.js index d55a9e6385..e6d04c8921 100644 --- a/src/webcomponents/file/file-view.js +++ b/src/webcomponents/file/file-view.js @@ -108,6 +108,7 @@ export default class FileView extends LitElement { .info(this.fileId, params) .then(response => { this.file = response.responses[0].results[0]; + // images = await UtilsNew.xyz(aaa) }) .catch(reason => { this.file = {}; @@ -209,15 +210,12 @@ export default class FileView extends LitElement { field: "size", type: "custom", display: { - render: field => html`${UtilsNew.getDiskUsage(field)}` + render: field => `${UtilsNew.getDiskUsage(field)}` }, }, { - title: "Format (Bioformat)", - type: "complex", - display: { - template: "${format} (${bioformat})", - }, + title: "Format", + field: "format", }, { title: "Tags", @@ -232,7 +230,7 @@ export default class FileView extends LitElement { field: "creationDate", type: "custom", display: { - render: field => html`${UtilsNew.dateFormatter(field)}`, + render: field => `${UtilsNew.dateFormatter(field)}`, }, }, { @@ -240,7 +238,7 @@ export default class FileView extends LitElement { field: "internal.status", type: "custom", display: { - render: field => field ? html`${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-", + render: field => field ? `${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-", }, }, { @@ -250,7 +248,7 @@ export default class FileView extends LitElement { type: "custom", display: { render: field => { - return field?.id ? html`${field.id} (${UtilsNew.dateFormatter(field.date)})` : "-"; + return field?.id ? `${field.id} (${UtilsNew.dateFormatter(field.date)})` : "-"; } }, }, diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index cb4398bc76..027c13461a 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -182,11 +182,21 @@ export default class IndividualView extends LitElement { elements: [ { title: "Individual ID", - type: "custom", + // type: "custom", + type: "complex", display: { - render: data => ` - ${data.id} (UUID: ${data.uuid}) - `, + // render: data => ` + // ${data.id} (UUID: ${data.uuid}) + // `, + template: "${id} (${uuid})", + // transform: { + // id: id => id.toLowerCase(), + // }, + style: { + id: { + "font-weight": "bold", + } + } }, }, { @@ -196,33 +206,38 @@ export default class IndividualView extends LitElement { { title: "Father ID", field: "father.id", - type: "basic", + // type: "basic", }, { title: "Mother ID", field: "mother.id", - type: "basic", + // type: "basic", }, { title: "Reported Sex (Karyotypic)", - type: "custom", + // type: "custom", + type: "complex", display: { - render: individual => ` - ${individual.sex?.id ?? individual.sex ?? "Not specified"} (${individual.karyotypicSex ?? "Not specified"}) - `, + // render: individual => ` + // ${individual.sex?.id ?? "Not specified"} (${individual.karyotypicSex ?? "Not specified"}) + // `, + defaultValue: "Not specified", + template: "${sex.id} (${karyotypicSex})" }, }, { title: "Inferred Karyotypic Sex", - type: "custom", + // type: "custom", + field: "qualityControl", display: { - render: data => { - if (data?.qualityControl?.inferredSexReports && data.qualityControl.inferredSexReports?.length > 0) { - return data.qualityControl.inferredSexReports[0].inferredKaryotypicSex; - } else { - return "-"; - } - }, + // render: data => { + // if (data?.qualityControl?.inferredSexReports?.length > 0) { + // return data.qualityControl.inferredSexReports[0].inferredKaryotypicSex; + // } else { + // return "-"; + // } + // }, + format: qualityControl => qualityControl?.inferredSexReports?.length > 0 ? qualityControl.inferredSexReports[0].inferredKaryotypicSex : "-" }, }, { @@ -235,7 +250,8 @@ export default class IndividualView extends LitElement { type: "list", display: { contentLayout: "vertical", - render: disorder => UtilsNew.renderHTML(CatalogGridFormatter.disorderFormatter(disorder)), + // render: disorder => CatalogGridFormatter.disorderFormatter(disorder), + format: disorder => CatalogGridFormatter.disorderFormatter(disorder), defaultValue: "N/A", }, }, @@ -244,17 +260,23 @@ export default class IndividualView extends LitElement { field: "phenotypes", type: "list", display: { - contentLayout: "bullets", - render: phenotype => { + contentLayout: "vertical", + // filter: phenotypes => [phenotypes[0]], + // transform: phenotypes => phenotypes.map(phenotype => { + // phenotype.id = phenotype.id.toLowerCase(); + // return phenotype; + // }), + // render: phenotype => { + format: phenotype => { let id = phenotype.id; if (phenotype.id.startsWith("HP:")) { - id = html` + id = ` ${phenotype.id} `; } - return html`${phenotype.name} (${id})`; + return `${phenotype.name} (${id})`; }, defaultValue: "N/A", }, @@ -269,26 +291,32 @@ export default class IndividualView extends LitElement { }, { title: "Status", - field: "internal.status", - type: "custom", + // field: "internal.status", + // type: "custom", + type: "complex", display: { - render: field => field ? `${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-" + // render: field => field ? `${field.name} (${UtilsNew.dateFormatter(field.date)})` : "-" + template: "${internal.status.name} (${internal.status.date})", + format: { + "internal.status.date": date => UtilsNew.dateFormatter(date) + } }, }, { title: "Creation Date", field: "creationDate", - type: "custom", + // type: "custom", display: { - render: field => field ? UtilsNew.dateFormatter(field) : "-" + // render: field => field ? UtilsNew.dateFormatter(field) : "-" + format: creationDate => UtilsNew.dateFormatter(creationDate) }, }, { title: "Modification Date", field: "modificationDate", - type: "custom", + // type: "custom", display: { - render: field => field ? UtilsNew.dateFormatter(field) : "-" + format: modificationDate => UtilsNew.dateFormatter(modificationDate), }, }, { @@ -308,23 +336,46 @@ export default class IndividualView extends LitElement { field: "samples", type: "table", display: { + className: "", + style: "", + headerClassName: "", + headerStyle: "", + headerVisible: true, + // filter: array => array.filter(item => item.somatic), + // transform: array => array.map(item => { + // item.somatic = true; + // return item; + // }), + defaultValue: "No phenotypes found", columns: [ { title: "Samples ID", field: "id", + display: { + style: { + "font-weight": "bold" + } + } }, { title: "Somatic", + type: "custom", field: "somatic", - formatter: value => value ? "true" : "false", + // formatter: value => value ? "true" : "false", + display: { + render: somatic => somatic ? "true" : "false", + style: { + color: "red" + } + } }, { title: "Phenotypes", field: "phenotypes", - formatter: value => value?.length ? `${value.map(d => d.id).join(", ")}` : "-", + type: "list" + // formatter: value => value?.length ? `${value.map(d => d.id).join(", ")}` : "-", }, ], - defaultValue: "No phenotypes found", }, }, ], diff --git a/src/webcomponents/job/job-view.js b/src/webcomponents/job/job-view.js index 30457e03cf..ea7e37f7b0 100644 --- a/src/webcomponents/job/job-view.js +++ b/src/webcomponents/job/job-view.js @@ -192,7 +192,7 @@ export default class JobView extends LitElement { name: "Status", type: "custom", display: { - render: job => UtilsNew.renderHTML(UtilsNew.jobStatusFormatter(job.internal.status, true)) + render: job => UtilsNew.jobStatusFormatter(job.internal.status, true) } }, { @@ -205,8 +205,8 @@ export default class JobView extends LitElement { type: "list", display: { separator: "", - render: tag => { - return html`${tag}`; + format: tag => { + return `${tag}`; } }, defaultValue: "-" @@ -215,7 +215,7 @@ export default class JobView extends LitElement { name: "Submitted Date", type: "custom", display: { - render: job => html`${UtilsNew.dateFormatter(job.creationDate, "D MMM YYYY, h:mm:ss a")}` + render: job => `${UtilsNew.dateFormatter(job.creationDate, "D MMM YYYY, h:mm:ss a")}` } }, { @@ -237,8 +237,8 @@ export default class JobView extends LitElement { render: job => { if (job.execution) { const start = job.execution.start ? moment(job.execution.start).format("D MMM YYYY, h:mm:ss a") : "-"; - const end = job.execution.end ? html`- ${moment(job.execution.end).format("D MMM YYYY, h:mm:ss a")}` : "-"; - return html`${start} - ${end}`; + const end = job.execution.end ? `- ${moment(job.execution.end).format("D MMM YYYY, h:mm:ss a")}` : "-"; + return `${start} - ${end}`; } else { return "-"; } @@ -251,18 +251,19 @@ export default class JobView extends LitElement { display: { render: job => { if (job.params) { - return Object.entries(job.params).map(([param, value]) => html` -
- : - ${value && typeof value === "object" ? html` -
    - ${Object.keys(value).map(key => html` -
  • ${key}: ${value[key] || "-"}
  • - `)} -
- ` : (value || "-")} -
- `); + return Object.entries(job.params) + .map(([param, value]) => ` +
+ : + ${value && typeof value === "object" ? ` +
    + ${Object.keys(value).map(key => ` +
  • ${key}: ${value[key] || "-"}
  • + `)} +
+ ` : (value || "-")} +
+ `); } else { return "-"; } @@ -291,7 +292,7 @@ export default class JobView extends LitElement { // CAUTION: Temporary patch for managing outputFiles array of nulls. // See details in: https://app.clickup.com/t/36631768/TASK-1704 if (job.output.length > 0 && job.output.every(jobOut => jobOut === null)) { - return html` + return ` ` : null} -
- ${section.elements.map(element => this._createElement(element, section))} -
+ ${content}
- ${buttonsSectionVisible ? this.renderButtons(null, section?.id) : null} + ${buttonsVisible ? this.renderButtons(null, section?.id) : null} `; } @@ -618,15 +680,14 @@ export default class DataForm extends LitElement { } } - // Only nested in 'object' and 'object-list', in these cases we do not want to create - // the rest of the HTML + // Only nested in 'object' and 'object-list', in these cases we do not want to create the rest of the HTML if (element?.display?.nested) { return content; } // Initialize element values const layout = this._getDefaultLayout(element, section); - const width = this._getWidth(element) || 12; + const width = this._getElementWidth(element, section) || 12; // Initialize container values const elementContainerClassName = element.display?.containerClassName ?? ""; @@ -832,6 +893,7 @@ export default class DataForm extends LitElement { // TODO to be fixed. if (element.field === "FILTER") { value = value === "PASS"; + // eslint-disable-next-line no-param-reassign element.text = "Include only PASS variants"; } @@ -946,7 +1008,7 @@ export default class DataForm extends LitElement { if (typeof element.allowedValues === "function") { let item; if (element.field?.includes("[]")) { - const match = element.field.match(DataForm.re); + const match = element.field.match(DataForm.ARRAY_FIELD_REGULAR_EXPRESSION); if (match) { item = UtilsNew.getObjectValue(this.data, match?.groups?.arrayFieldName, "")[match?.groups?.index]; } @@ -1099,13 +1161,13 @@ export default class DataForm extends LitElement { _createTableElement(element, section) { // Get valid parameters - let array = this.getValue(element.field, null, element.defaultValue); + let array = this.getValue(element.field); const tableClassName = element.display?.className || ""; const tableStyle = this._parseStyleField(element.display?.style) || ""; const headerClassName = element.display?.headerClassName || ""; const headerStyle = this._parseStyleField(element.display?.headerStyle) || ""; const headerVisible = this._getBooleanValue(element.display?.headerVisible, true); - const errorMessage = this._getErrorMessage(element); + const errorMessage = this._getDefaultErrorMessage(element, section); const errorClassName = element.display?.errorClassName ?? element.display?.errorClasses ?? "text-danger"; // 1. Check field exists, and it is an array. Also, check 'columns' is defined @@ -1267,7 +1329,7 @@ export default class DataForm extends LitElement { `; return this._createElementTemplate(element, null, content); } else { - const message = this._getErrorMessage(element); + const message = this._getDefaultErrorMessage(element); const errorClassName = element.display?.errorClassName ?? element.display?.errorClasses ?? "text-danger"; return this._createElementTemplate(element, null, null, { message: message, @@ -1357,7 +1419,7 @@ export default class DataForm extends LitElement { // When an object-list, get the item being validated. let item; if (element.field?.includes("[]")) { - const match = element.field.match(DataForm.re); + const match = element.field.match(DataForm.ARRAY_FIELD_REGULAR_EXPRESSION); if (match) { item = UtilsNew.getObjectValue(this.data, match?.groups?.arrayFieldName, "")[match?.groups?.index]; } @@ -1372,7 +1434,7 @@ export default class DataForm extends LitElement { if (content) { return this._createElementTemplate(element, data, content); } else { - const message = this._getErrorMessage(element); + const message = this._getDefaultErrorMessage(element); const errorClassName = element.display?.errorClassName ?? element.display?.errorClasses ?? "text-danger"; return this._createElementTemplate(element, null, null, { message: message, @@ -1703,7 +1765,7 @@ export default class DataForm extends LitElement { if (typeof element.save === "function") { let currentValue; if (element.field.includes("[]")) { - const match = element.field.match(DataForm.re); + const match = element.field.match(DataForm.ARRAY_FIELD_REGULAR_EXPRESSION); if (match) { currentValue = UtilsNew.getObjectValue(this.data, match?.groups?.arrayFieldName, "")[match?.groups?.index]; } diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index a4e9cf36e9..d71f6d7b17 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -178,6 +178,30 @@ export default class IndividualView extends LitElement { collapsed: false, display: { visible: individual => individual?.id, + // layout: [ + // { + // id: "name", + // className: "" + // }, + // { + // id: "", + // className: "row", + // elements: [ + // { + // id: "father", + // className: "col-md-6" + // }, + // { + // id: "mother", + // className: "col-md-6" + // } + // ] + // }, + // { + // id: "sex", + // className: "" + // }, + // ] }, elements: [ { @@ -200,20 +224,24 @@ export default class IndividualView extends LitElement { }, }, { + id: "name", title: "Name", field: "name", }, { + id: "father", title: "Father ID", field: "father.id", // type: "basic", }, { + id: "mother", title: "Mother ID", field: "mother.id", // type: "basic", }, { + id: "sex", title: "Reported Sex (Karyotypic)", // type: "custom", type: "complex", From a2b7b6bdd9786e64afc97a5b6b61f56ee5dedaa2 Mon Sep 17 00:00:00 2001 From: imedina Date: Tue, 7 Nov 2023 23:50:00 +0000 Subject: [PATCH 025/153] viz: add new Signature class to render mutational signatures histograms --- src/core/visualisation/signature.js | 584 ++++++++++++++++++ src/webcomponents/commons/forms/data-form.js | 65 +- .../commons/view/signature-view.js | 525 ++++++++-------- 3 files changed, 883 insertions(+), 291 deletions(-) create mode 100644 src/core/visualisation/signature.js diff --git a/src/core/visualisation/signature.js b/src/core/visualisation/signature.js new file mode 100644 index 0000000000..7b5f434b43 --- /dev/null +++ b/src/core/visualisation/signature.js @@ -0,0 +1,584 @@ +/* + * Copyright 2015-2016 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import UtilsNew from "../utils-new.js"; + +export default class Signature { + + constructor(signature, mode, config) { + this.signature = signature; + this.mode = mode; + + this._config = {...this.getDefaultConfig(), ...config}; + } + + render(targetDiv, config) { + if (config) { + this._config = {...this.getDefaultConfig(), ...config}; + } + + return this.#renderCounts(targetDiv); + } + + #renderCounts(targetDiv) { + if (!this.signature || this.signature?.errorState) { + return; + } + + const mode = this.mode.toUpperCase(); + const counts = this.signature?.counts || []; + const categories = counts.map(point => point?.context); + const data = counts.map(point => point?.total); + + const substitutionClass = string => { + const [, pair, letter] = string.match(/[ACTG]\[(([ACTG])>[ACTG])\][ACTG]+/); + return {pair, letter}; + }; + + const rearragementClass = string => { + const fields = string.split("_"); + const pair = fields[0] + "_" + fields[1]; + const letter = fields[2] || ""; + return {pair, letter}; + }; + + const dataset = { + "C>A": { + color: "#31bef0", + data: [] + }, + "C>G": { + color: "#000000", + data: [] + }, + "C>T": { + color: "#e62725", + data: [] + }, + "T>A": { + color: "#cbcacb", + data: [] + }, + "T>C": { + color: "#a1cf63", + data: [] + }, + "T>G": { + color: "#edc8c5", + data: [] + }, + "clustered_del": { + color: "#e41a1c", + data: [], + label: "del", + group: "clustered", + }, + "clustered_tds": { + color: "#4daf4a", + data: [], + label: "tds", + group: "clustered", + }, + "clustered_inv": { + color: "#377eb8", + data: [], + label: "inv", + group: "clustered", + }, + "clustered_trans": { + color: "#984ea3", + data: [], + label: "tr", + group: "clustered", + }, + "non-clustered_del": { + color: "#e41a1c", + data: [], + label: "del", + group: "non-clustered", + }, + "non-clustered_tds": { + color: "#4daf4a", + data: [], + label: "tds", + group: "non-clustered", + }, + "non-clustered_inv": { + color: "#377eb8", + data: [], + label: "inv", + group: "non-clustered", + }, + "non-clustered_trans": { + color: "#984ea3", + data: [], + label: "tr", + group: "non-clustered", + }, + }; + + const groups = { + "clustered": { + bgColor: "#000", + textColor: "#fff", + }, + "non-clustered": { + bgColor: "#f0f0f0", + textColor: "#000", + }, + }; + + counts.forEach(count => { + if (count) { + if (this.mode === "SBS") { + const {pair} = substitutionClass(count.context); + dataset[pair].data.push(count.total); + } else { + const {pair} = rearragementClass(count.context); + dataset[pair].data.push(count.total); + } + } + }); + + const addRects = function (chart) { + $(".rect", chart.renderTo).remove(); + $(".rect-label", chart.renderTo).remove(); + + const totalLength = Object.values(dataset).reduce((sum, item) => sum + item.data.length, 0); + const top = mode === "SBS" ? 50 : chart.plotTop + chart.plotHeight; + let lastWidth = 0; + let lastGroup = null; + let lastGroupWidth = 0; + + Object.keys(dataset).forEach(k => { + if (dataset[k].data.length === 0) { + return; + } + + // Display group + if (dataset[k].group && lastGroup !== dataset[k].group) { + const group = dataset[k].group; + const groupLength = Object.values(dataset).reduce((sum, item) => { + return item.group === group ? sum + item.data.length : sum; + }, 0); + const groupWidth = groupLength * (chart.plotWidth / totalLength); + const groupHeight = 20; + const groupPosition = top + 22; + + // Render group background + chart.renderer + .rect(chart.plotLeft + lastGroupWidth, groupPosition, groupWidth, groupHeight, 0) + .attr({ + fill: groups[group].bgColor, + zIndex: 2, + }) + .addClass("rect") + .add(); + + // Render group label + chart.renderer + .text(group, chart.plotLeft + lastGroupWidth + groupWidth / 2, groupPosition + groupHeight / 2) + .css({ + color: groups[group].textColor, + fontSize: "13px", + }) + .attr({ + "dominant-baseline": "middle", + "text-anchor": "middle", + "zIndex": 3, + }) + .addClass("rect-label") + .add(); + + lastGroup = group; + lastGroupWidth = lastGroupWidth + groupWidth; + } + + const width = dataset[k].data.length * (chart.plotWidth / totalLength); + const height = 20; + const position = top + 2; + + chart.renderer + .rect(chart.plotLeft + lastWidth, position, width, height, 0) + .attr({ + fill: dataset[k].color, + zIndex: 2 + }) + .addClass("rect") + .add(); + + chart.renderer + .text(dataset[k].label || k, chart.plotLeft + lastWidth + width / 2, position + height / 2) + .css({ + color: "#fff", + fontSize: "13px", + }) + .attr({ + "dominant-baseline": "middle", + "text-anchor": "middle", + "zIndex": 3, + }) + .addClass("rect-label") + .add(); + + lastWidth = lastWidth + width; + }); + }; + + $(`#${targetDiv}`).highcharts({ + title: { + text: `${counts.reduce((c, s) => c + s.total, 0)} ${mode === "SBS" ? "Substitutions New" : "Rearrangements"}`, + }, + chart: { + height: this._config.height, // use plain CSS to avoid resize when is visible + type: "column", + events: { + redraw: function () { + addRects(this); + }, + load: function () { + addRects(this); + } + }, + marginTop: mode === "SBS" ? 80 : 50, + }, + credits: { + enabled: false + }, + legend: { + enabled: false + }, + tooltip: { + formatter: function () { + if (this.x.includes("[")) { + const {pair, letter} = substitutionClass(this.x); + return this.x.replace(pair, `${letter}`).replace("[", "").replace("]", "") + `: ${this.y}`; + } else { + return `${this.x}: ${this.y}`; + } + } + }, + xAxis: { + categories: categories, + labels: { + // xAxis labels are disabled in SBS mode + enabled: mode !== "SBS", + rotation: -90, + formatter: data => { + if (mode === "SBS") { + const {pair, letter} = substitutionClass(data.value); + return data.value.replace(pair, `${letter}`).replace("[", "").replace("]", ""); + } else { + return data.value.split("_")[2]; + } + }, + y: mode === "SBS" ? 10 : 50, + } + }, + colors: Object.keys(dataset).flatMap(key => Array(dataset[key].data.length).fill(dataset[key].color)), + series: [{ + colorByPoint: "true", + data: data + }] + }); + } + + #renderFitting() { + const self = this; + const fitting = (this.signature?.fittings || []).find(fitting => fitting.id === this.fittingId); + const scores = fitting?.scores; + + if (!scores || scores.length === 0) { + return; + } + + $(`#${this._prefix}SignatureFittingPlot`).highcharts({ + chart: { + height: this._config.height, + type: "bar", + }, + title: null, + credits: { + enabled: false + }, + legend: { + enabled: false + }, + xAxis: { + categories: scores.map(score => score.label || score.signatureId), + labels: { + formatter: function () { + if (this.value && self._config?.mutationalSignature?.[this.value]) { + const id = self._config?.mutationalSignature?.[this.value]; + return ` + + ${this.value} + + `; + } else { + return this.value; + } + }, + }, + }, + yAxis: { + min: 0, + title: null, + }, + series: [{ + data: scores.map(score => score.value), + name: "Value", + }] + }, chart => { + // Allow opening the link to Signal in another window + // See TASK-1068 + Array.from(chart.container.querySelectorAll("a")).forEach(link => { + link.addEventListener("click", event => { + event.preventDefault(); + window.open(link.getAttribute("href"), "_blank"); + }); + }); + }); + } + + getDefaultConfig() { + return { + // width: null, width is always 100% of the visible container + height: 320, + mutationalSignatureUrl: "https://signal.mutationalsignatures.com/explore/referenceCancerSignature/", + mutationalSignature: { + "RefSig R2": "1", + "RefSig R9": "2", + "RefSig R10": "3", + "RefSig R11": "4", + "RefSig R12": "5", + "RefSig R13": "6", + "RefSig R14": "7", + "RefSig R15": "8", + "RefSig R16": "9", + "RefSig R17": "10", + "RefSig R18": "11", + "RefSig R4": "12", + "RefSig R19": "13", + "RefSig R20": "14", + "RefSig R6a": "15", + "RefSig R1": "16", + "RefSig R7": "17", + "RefSig R5": "18", + "RefSig R6b": "19", + "RefSig R8": "20", + "RefSig R3": "21", + "RefSig 1": "22", + "RefSig 3": "23", + "RefSig 30": "24", + "RefSig 4": "25", + "RefSig N1": "26", + "RefSig PLATINUM": "27", + "RefSig 33": "28", + "RefSig 22": "29", + "RefSig 10": "30", + "RefSig 18": "31", + "RefSig 36": "32", + "RefSig 7": "33", + "RefSig 16": "34", + "RefSig 19": "35", + "RefSig N2": "36", + "RefSig 9": "37", + "RefSig N3": "38", + "RefSig 52": "39", + "RefSig 11": "40", + "RefSig 17": "41", + "RefSig 38": "42", + "RefSig 51": "43", + "RefSig N4": "44", + "RefSig N5": "45", + "RefSig N6": "46", + "RefSig N7": "47", + "RefSig N8": "48", + "RefSig N9": "49", + "RefSig N10": "50", + "RefSig N11": "51", + "RefSig MMR1": "52", + "RefSig 24": "53", + "RefSig N12": "54", + "RefSig 2": "55", + "RefSig MMR2": "56", + "RefSig 5": "57", + "RefSig 8": "58", + "RefSig 13": "59", + "SBS1": "60", + "SBS2": "61", + "SBS3": "62", + "SBS4": "63", + "SBS5": "64", + "SBS6": "65", + "SBS7a": "66", + "SBS7c": "67", + "SBS8": "68", + "SBS9": "69", + "SBS10a": "70", + "SBS10d": "71", + "SBS11": "72", + "SBS12": "73", + "SBS13": "74", + "SBS14": "75", + "SBS15": "76", + "SBS16": "77", + "SBS17": "78", + "SBS17a": "79", + "SBS18": "80", + "SBS19": "81", + "SBS20": "82", + "SBS22": "83", + "SBS23": "84", + "SBS24": "85", + "SBS26": "86", + "SBS28": "87", + "SBS30": "88", + "SBS31": "89", + "SBS32": "90", + "SBS33": "91", + "SBS35": "92", + "SBS38": "93", + "SBS44": "94", + "SBS52": "95", + "SBS53": "96", + "SBS57": "97", + "SBS84": "98", + "SBS87": "99", + "SBS88": "100", + "SBS90": "101", + "SBS92": "102", + "SBS93": "103", + "SBS94": "104", + "SBS95": "105", + "SBS96": "106", + "SBS97": "107", + "SBS98": "108", + "SBS99": "109", + "SBS100": "110", + "SBS101": "111", + "SBS102": "112", + "SBS103": "113", + "SBS104": "114", + "SBS105": "115", + "SBS106": "116", + "SBS107": "117", + "SBS108": "118", + "SBS109": "119", + "SBS110": "120", + "SBS111": "121", + "SBS112": "122", + "SBS113": "123", + "SBS114": "124", + "SBS115": "125", + "SBS116": "126", + "SBS117": "127", + "SBS118": "128", + "SBS119": "129", + "SBS120": "130", + "SBS121": "131", + "SBS122": "132", + "SBS123": "133", + "SBS124": "134", + "SBS125": "135", + "SBS126": "136", + "SBS127": "137", + "SBS128": "138", + "SBS129": "139", + "SBS130": "140", + "SBS131": "141", + "SBS132": "142", + "SBS133": "143", + "SBS134": "144", + "SBS135": "145", + "SBS136": "146", + "SBS137": "147", + "SBS138": "148", + "SBS139": "149", + "SBS140": "150", + "SBS141": "151", + "SBS142": "152", + "SBS143": "153", + "SBS144": "154", + "SBS145": "155", + "SBS146": "156", + "SBS147": "157", + "SBS148": "158", + "SBS149": "159", + "SBS150": "160", + "SBS151": "161", + "SBS152": "162", + "SBS153": "163", + "SBS154": "164", + "SBS155": "165", + "SBS156": "166", + "SBS157": "167", + "SBS158": "168", + "SBS159": "169", + "SBS160": "170", + "SBS161": "171", + "SBS162": "172", + "SBS163": "173", + "SBS164": "174", + "SBS165": "175", + "SBS166": "176", + "SBS167": "177", + "SBS168": "178", + "SBS169": "179", + "DBS1": "180", + "DBS2": "181", + "DBS3": "182", + "DBS4": "183", + "DBS5": "184", + "DBS7": "185", + "DBS8": "186", + "DBS10": "187", + "DBS11": "188", + "DBS12": "189", + "DBS13": "190", + "DBS14": "191", + "DBS15": "192", + "DBS16": "193", + "DBS17": "194", + "DBS18": "195", + "DBS19": "196", + "DBS20": "197", + "DBS21": "198", + "DBS22": "199", + "DBS23": "200", + "DBS24": "201", + "DBS25": "202", + "DBS26": "203", + "DBS27": "204", + "DBS28": "205", + "DBS29": "206", + "DBS30": "207", + "DBS31": "208", + "DBS32": "209", + "DBS33": "210", + "DBS34": "211", + "DBS35": "212", + "DBS36": "213", + "DBS37": "214", + "DBS38": "215", + "DBS39": "216", + "DBS40": "217", + "DBS41": "218" + } + }; + } +} diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 0658413f8b..0f50e60483 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -606,11 +606,42 @@ export default class DataForm extends LitElement { } else { // Other 'type' are rendered by specific functions switch (element.type) { + // View elements case "text": case "title": case "notification": content = this._createTextElement(element); break; + case "complex": + content = this._createComplexElement(element, this.data, section); + break; + case "list": + content = this._createListElement(element, this.data, section); + break; + case "table": + content = this._createTableElement(element, section); + break; + case "image": + content = this._createImageElement(element); + break; + case "chart": + case "plot": + content = this._createPlotElement(element); + break; + case "json": + content = this._createJsonElement(element); + break; + case "tree": + content = this._createTreeElement(element); + break; + case "download": + content = this._createDownloadElement(element); + break; + case "custom": + content = html`${this._createCustomElement(element)}`; + break; + + // Form controls and editors case "input-text": content = this._createInputElement(element, "text", section); break; @@ -629,46 +660,18 @@ export default class DataForm extends LitElement { case "checkbox": content = this._createCheckboxElement(element); break; + case "select": + content = this._createInputSelectElement(element); + break; case "toggle-switch": content = this._createToggleSwitchElement(element); break; case "toggle-buttons": content = this._createToggleButtonsElement(element); break; - case "select": - content = this._createInputSelectElement(element); - break; - case "complex": - content = this._createComplexElement(element, this.data, section); - break; - case "list": - content = this._createListElement(element, this.data, section); - break; - case "table": - content = this._createTableElement(element, section); - break; - case "image": - content = this._createImageElement(element); - break; - case "chart": - case "plot": - content = this._createPlotElement(element); - break; - case "json": - content = this._createJsonElement(element); - break; case "json-editor": content = this._createJsonEditorElement(element); break; - case "tree": - content = this._createTreeElement(element); - break; - case "custom": - content = html`${this._createCustomElement(element)}`; - break; - case "download": - content = this._createDownloadElement(element); - break; case "object": content = this._createObjectElement(element); break; diff --git a/src/webcomponents/commons/view/signature-view.js b/src/webcomponents/commons/view/signature-view.js index 81ed931800..395be1a297 100644 --- a/src/webcomponents/commons/view/signature-view.js +++ b/src/webcomponents/commons/view/signature-view.js @@ -17,6 +17,7 @@ import {LitElement, html} from "lit"; import UtilsNew from "../../../core/utils-new.js"; import "../../loading-spinner.js"; +import Signature from "../../../core/visualisation/signature"; export default class SignatureView extends LitElement { @@ -74,266 +75,270 @@ export default class SignatureView extends LitElement { } signatureCountsObserver() { - if (!this.signature || this.signature?.errorState) { - return; - } - - const mode = this.mode.toUpperCase(); - const counts = this.signature?.counts || []; - const categories = counts.map(point => point?.context); - const data = counts.map(point => point?.total); - - const substitutionClass = string => { - const [, pair, letter] = string.match(/[ACTG]\[(([ACTG])>[ACTG])\][ACTG]+/); - return {pair, letter}; - }; - - const rearragementClass = string => { - const fields = string.split("_"); - const pair = fields[0] + "_" + fields[1]; - const letter = fields[2] || ""; - return {pair, letter}; - }; - - const dataset = { - "C>A": { - color: "#31bef0", - data: [] - }, - "C>G": { - color: "#000000", - data: [] - }, - "C>T": { - color: "#e62725", - data: [] - }, - "T>A": { - color: "#cbcacb", - data: [] - }, - "T>C": { - color: "#a1cf63", - data: [] - }, - "T>G": { - color: "#edc8c5", - data: [] - }, - "clustered_del": { - color: "#e41a1c", - data: [], - label: "del", - group: "clustered", - }, - "clustered_tds": { - color: "#4daf4a", - data: [], - label: "tds", - group: "clustered", - }, - "clustered_inv": { - color: "#377eb8", - data: [], - label: "inv", - group: "clustered", - }, - "clustered_trans": { - color: "#984ea3", - data: [], - label: "tr", - group: "clustered", - }, - "non-clustered_del": { - color: "#e41a1c", - data: [], - label: "del", - group: "non-clustered", - }, - "non-clustered_tds": { - color: "#4daf4a", - data: [], - label: "tds", - group: "non-clustered", - }, - "non-clustered_inv": { - color: "#377eb8", - data: [], - label: "inv", - group: "non-clustered", - }, - "non-clustered_trans": { - color: "#984ea3", - data: [], - label: "tr", - group: "non-clustered", - }, - }; - - const groups = { - "clustered": { - bgColor: "#000", - textColor: "#fff", - }, - "non-clustered": { - bgColor: "#f0f0f0", - textColor: "#000", - }, - }; - - counts.forEach(count => { - if (count) { - if (this.mode === "SBS") { - const {pair} = substitutionClass(count.context); - dataset[pair].data.push(count.total); - } else { - const {pair} = rearragementClass(count.context); - dataset[pair].data.push(count.total); - } - } - }); - - const addRects = function (chart) { - $(".rect", chart.renderTo).remove(); - $(".rect-label", chart.renderTo).remove(); - - const totalLength = Object.values(dataset).reduce((sum, item) => sum + item.data.length, 0); - const top = mode === "SBS" ? 50 : chart.plotTop + chart.plotHeight; - let lastWidth = 0; - let lastGroup = null; - let lastGroupWidth = 0; - - Object.keys(dataset).forEach(k => { - if (dataset[k].data.length === 0) { - return; - } - - // Display group - if (dataset[k].group && lastGroup !== dataset[k].group) { - const group = dataset[k].group; - const groupLength = Object.values(dataset).reduce((sum, item) => { - return item.group === group ? sum + item.data.length : sum; - }, 0); - const groupWidth = groupLength * (chart.plotWidth / totalLength); - const groupHeight = 20; - const groupPosition = top + 22; - - // Render group background - chart.renderer - .rect(chart.plotLeft + lastGroupWidth, groupPosition, groupWidth, groupHeight, 0) - .attr({ - fill: groups[group].bgColor, - zIndex: 2, - }) - .addClass("rect") - .add(); - - // Render group label - chart.renderer - .text(group, chart.plotLeft + lastGroupWidth + groupWidth / 2, groupPosition + groupHeight / 2) - .css({ - color: groups[group].textColor, - fontSize: "13px", - }) - .attr({ - "dominant-baseline": "middle", - "text-anchor": "middle", - "zIndex": 3, - }) - .addClass("rect-label") - .add(); - - lastGroup = group; - lastGroupWidth = lastGroupWidth + groupWidth; - } - - const width = dataset[k].data.length * (chart.plotWidth / totalLength); - const height = 20; - const position = top + 2; - - chart.renderer - .rect(chart.plotLeft + lastWidth, position, width, height, 0) - .attr({ - fill: dataset[k].color, - zIndex: 2 - }) - .addClass("rect") - .add(); - - chart.renderer - .text(dataset[k].label || k, chart.plotLeft + lastWidth + width / 2, position + height / 2) - .css({ - color: "#fff", - fontSize: "13px", - }) - .attr({ - "dominant-baseline": "middle", - "text-anchor": "middle", - "zIndex": 3, - }) - .addClass("rect-label") - .add(); - - lastWidth = lastWidth + width; - }); - }; - - $(`#${this._prefix}SignatureCountsPlot`).highcharts({ - title: { - text: `${counts.reduce((c, s) => c + s.total, 0)} ${mode === "SBS" ? "Substitutions" : "Rearrangements"}`, - }, - chart: { - height: this._config.height, // use plain CSS to avoid resize when is visible - type: "column", - events: { - redraw: function () { - addRects(this); - }, - load: function () { - addRects(this); - } - }, - marginTop: mode === "SBS" ? 80 : 50, - }, - credits: { - enabled: false - }, - legend: { - enabled: false - }, - tooltip: { - formatter: function () { - if (this.x.includes("[")) { - const {pair, letter} = substitutionClass(this.x); - return this.x.replace(pair, `${letter}`).replace("[", "").replace("]", "") + `: ${this.y}`; - } else { - return `${this.x}: ${this.y}`; - } - } - }, - xAxis: { - categories: categories, - labels: { - // xAxis labels are disabled in SBS mode - enabled: mode !== "SBS", - rotation: -90, - formatter: data => { - if (mode === "SBS") { - const {pair, letter} = substitutionClass(data.value); - return data.value.replace(pair, `${letter}`).replace("[", "").replace("]", ""); - } else { - return data.value.split("_")[2]; - } - }, - y: mode === "SBS" ? 10 : 50, - } - }, - colors: Object.keys(dataset).flatMap(key => Array(dataset[key].data.length).fill(dataset[key].color)), - series: [{ - colorByPoint: "true", - data: data - }] - }); + const signature = new Signature(this.signature, this.mode); + signature.render(`${this._prefix}SignatureCountsPlot`); + + + // if (!this.signature) { + // return; + // } + // + // const mode = this.mode.toUpperCase(); + // const counts = this.signature?.counts || []; + // const categories = counts.map(point => point?.context); + // const data = counts.map(point => point?.total); + // + // const substitutionClass = string => { + // const [, pair, letter] = string.match(/[ACTG]\[(([ACTG])>[ACTG])\][ACTG]+/); + // return {pair, letter}; + // }; + // + // const rearragementClass = string => { + // const fields = string.split("_"); + // const pair = fields[0] + "_" + fields[1]; + // const letter = fields[2] || ""; + // return {pair, letter}; + // }; + // + // const dataset = { + // "C>A": { + // color: "#31bef0", + // data: [] + // }, + // "C>G": { + // color: "#000000", + // data: [] + // }, + // "C>T": { + // color: "#e62725", + // data: [] + // }, + // "T>A": { + // color: "#cbcacb", + // data: [] + // }, + // "T>C": { + // color: "#a1cf63", + // data: [] + // }, + // "T>G": { + // color: "#edc8c5", + // data: [] + // }, + // "clustered_del": { + // color: "#e41a1c", + // data: [], + // label: "del", + // group: "clustered", + // }, + // "clustered_tds": { + // color: "#4daf4a", + // data: [], + // label: "tds", + // group: "clustered", + // }, + // "clustered_inv": { + // color: "#377eb8", + // data: [], + // label: "inv", + // group: "clustered", + // }, + // "clustered_trans": { + // color: "#984ea3", + // data: [], + // label: "tr", + // group: "clustered", + // }, + // "non-clustered_del": { + // color: "#e41a1c", + // data: [], + // label: "del", + // group: "non-clustered", + // }, + // "non-clustered_tds": { + // color: "#4daf4a", + // data: [], + // label: "tds", + // group: "non-clustered", + // }, + // "non-clustered_inv": { + // color: "#377eb8", + // data: [], + // label: "inv", + // group: "non-clustered", + // }, + // "non-clustered_trans": { + // color: "#984ea3", + // data: [], + // label: "tr", + // group: "non-clustered", + // }, + // }; + // + // const groups = { + // "clustered": { + // bgColor: "#000", + // textColor: "#fff", + // }, + // "non-clustered": { + // bgColor: "#f0f0f0", + // textColor: "#000", + // }, + // }; + // + // counts.forEach(count => { + // if (count) { + // if (this.mode === "SBS") { + // const {pair} = substitutionClass(count.context); + // dataset[pair].data.push(count.total); + // } else { + // const {pair} = rearragementClass(count.context); + // dataset[pair].data.push(count.total); + // } + // } + // }); + // + // const addRects = function (chart) { + // $(".rect", chart.renderTo).remove(); + // $(".rect-label", chart.renderTo).remove(); + // + // const totalLength = Object.values(dataset).reduce((sum, item) => sum + item.data.length, 0); + // const top = mode === "SBS" ? 50 : chart.plotTop + chart.plotHeight; + // let lastWidth = 0; + // let lastGroup = null; + // let lastGroupWidth = 0; + // + // Object.keys(dataset).forEach(k => { + // if (dataset[k].data.length === 0) { + // return; + // } + // + // // Display group + // if (dataset[k].group && lastGroup !== dataset[k].group) { + // const group = dataset[k].group; + // const groupLength = Object.values(dataset).reduce((sum, item) => { + // return item.group === group ? sum + item.data.length : sum; + // }, 0); + // const groupWidth = groupLength * (chart.plotWidth / totalLength); + // const groupHeight = 20; + // const groupPosition = top + 22; + // + // // Render group background + // chart.renderer + // .rect(chart.plotLeft + lastGroupWidth, groupPosition, groupWidth, groupHeight, 0) + // .attr({ + // fill: groups[group].bgColor, + // zIndex: 2, + // }) + // .addClass("rect") + // .add(); + // + // // Render group label + // chart.renderer + // .text(group, chart.plotLeft + lastGroupWidth + groupWidth / 2, groupPosition + groupHeight / 2) + // .css({ + // color: groups[group].textColor, + // fontSize: "13px", + // }) + // .attr({ + // "dominant-baseline": "middle", + // "text-anchor": "middle", + // "zIndex": 3, + // }) + // .addClass("rect-label") + // .add(); + // + // lastGroup = group; + // lastGroupWidth = lastGroupWidth + groupWidth; + // } + // + // const width = dataset[k].data.length * (chart.plotWidth / totalLength); + // const height = 20; + // const position = top + 2; + // + // chart.renderer + // .rect(chart.plotLeft + lastWidth, position, width, height, 0) + // .attr({ + // fill: dataset[k].color, + // zIndex: 2 + // }) + // .addClass("rect") + // .add(); + // + // chart.renderer + // .text(dataset[k].label || k, chart.plotLeft + lastWidth + width / 2, position + height / 2) + // .css({ + // color: "#fff", + // fontSize: "13px", + // }) + // .attr({ + // "dominant-baseline": "middle", + // "text-anchor": "middle", + // "zIndex": 3, + // }) + // .addClass("rect-label") + // .add(); + // + // lastWidth = lastWidth + width; + // }); + // }; + // + // $(`#${this._prefix}SignatureCountsPlot`).highcharts({ + // title: { + // text: `${counts.reduce((c, s) => c + s.total, 0)} ${mode === "SBS" ? "Substitutions" : "Rearrangements"}`, + // }, + // chart: { + // height: this._config.height, // use plain CSS to avoid resize when is visible + // type: "column", + // events: { + // redraw: function () { + // addRects(this); + // }, + // load: function () { + // addRects(this); + // } + // }, + // marginTop: mode === "SBS" ? 80 : 50, + // }, + // credits: { + // enabled: false + // }, + // legend: { + // enabled: false + // }, + // tooltip: { + // formatter: function () { + // if (this.x.includes("[")) { + // const {pair, letter} = substitutionClass(this.x); + // return this.x.replace(pair, `${letter}`).replace("[", "").replace("]", "") + `: ${this.y}`; + // } else { + // return `${this.x}: ${this.y}`; + // } + // } + // }, + // xAxis: { + // categories: categories, + // labels: { + // // xAxis labels are disabled in SBS mode + // enabled: mode !== "SBS", + // rotation: -90, + // formatter: data => { + // if (mode === "SBS") { + // const {pair, letter} = substitutionClass(data.value); + // return data.value.replace(pair, `${letter}`).replace("[", "").replace("]", ""); + // } else { + // return data.value.split("_")[2]; + // } + // }, + // y: mode === "SBS" ? 10 : 50, + // } + // }, + // colors: Object.keys(dataset).flatMap(key => Array(dataset[key].data.length).fill(dataset[key].color)), + // series: [{ + // colorByPoint: "true", + // data: data + // }] + // }); } signatureFittingObserver() { From f5a549e3e5f12e3b5d41bf0167c76685978cf375 Mon Sep 17 00:00:00 2001 From: imedina Date: Wed, 8 Nov 2023 12:54:35 +0000 Subject: [PATCH 026/153] wc: add initial VariantTableFormatter implementation --- .../variant-browser-grid-test.js | 74 + src/webcomponents/commons/forms/data-form.js | 8 +- .../variant/variant-table-formatter.js | 1472 +++++++++++++++++ 3 files changed, 1550 insertions(+), 4 deletions(-) create mode 100644 src/webcomponents/variant/variant-table-formatter.js diff --git a/src/sites/test-app/webcomponents/variant-browser-grid-test.js b/src/sites/test-app/webcomponents/variant-browser-grid-test.js index b588654b67..47c9e5d917 100644 --- a/src/sites/test-app/webcomponents/variant-browser-grid-test.js +++ b/src/sites/test-app/webcomponents/variant-browser-grid-test.js @@ -20,8 +20,12 @@ import {html, LitElement} from "lit"; import {DATA_FORM_EXAMPLE} from "../conf/data-form.js"; import UtilsNew from "../../../core/utils-new.js"; +import "../../../webcomponents/commons/forms/data-form.js"; import "../../../webcomponents/loading-spinner.js"; import "../../../webcomponents/variant/variant-browser-grid.js"; +import Types from "../../../webcomponents/commons/types"; +import CatalogGridFormatter from "../../../webcomponents/commons/catalog-grid-formatter"; +import VariantTableFormatter from "../../../webcomponents/variant/variant-table-formatter"; class VariantBrowserGridTest extends LitElement { @@ -125,11 +129,17 @@ class VariantBrowserGridTest extends LitElement { if (this.isLoading) { return html``; } + return html`

Variant Browser (${this.testVariantFile?.split("-")?.at(-1)})

+ + + array.filter(item => item.somatic), + // transform: array => array.map(item => { + // item.somatic = true; + // return item; + // }), + defaultValue: "-", + columns: [ + VariantTableFormatter.variantFormatter(), + VariantTableFormatter.geneFormatter() + // { + // title: "Somatic", + // type: "custom", + // field: "somatic", + // // formatter: value => value ? "true" : "false", + // display: { + // render: somatic => somatic ? "true" : "false", + // style: { + // color: "red" + // } + // } + // }, + // { + // title: "Phenotypes", + // field: "phenotypes", + // type: "list" + // // formatter: value => value?.length ? `${value.map(d => d.id).join(", ")}` : "-", + // }, + ], + }, + }, + ], + }, + ], + }; + } + } customElements.define("variant-browser-grid-test", VariantBrowserGridTest); diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 0f50e60483..b006beb4b9 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -134,7 +134,7 @@ export default class DataForm extends LitElement { if (value && display?.format) { // check if response is actually an HTML // value = UtilsNew.renderHTML(display.format(value)); - value = display.format(value); + value = display.format(value, object); } // Needed for handling falsy values @@ -189,7 +189,7 @@ export default class DataForm extends LitElement { // }, // }, if (element?.display?.format?.[match]) { - value = element?.display?.format?.[match](value); + value = element?.display?.format?.[match](value, object); } if (element?.display?.style?.[match]) { const style = this._parseStyleField(element?.display?.style?.[match]); @@ -1120,9 +1120,9 @@ export default class DataForm extends LitElement { if (element.display?.format || element.display?.render) { // NOTE: 'element.display.render' is now deprecated, use 'format' instead if (element.display?.format) { - values = array.map(item => element.display.format(item)); + values = array.map(item => element.display.format(item, data)); } else { - values = array.map(item => element.display.render(item)); + values = array.map(item => element.display.render(item, data)); } } else { if (element.display?.template) { diff --git a/src/webcomponents/variant/variant-table-formatter.js b/src/webcomponents/variant/variant-table-formatter.js new file mode 100644 index 0000000000..f1a2dbd9fb --- /dev/null +++ b/src/webcomponents/variant/variant-table-formatter.js @@ -0,0 +1,1472 @@ +/* + * Copyright 2015-2016 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; +import VariantInterpreterGridFormatter from "./interpretation/variant-interpreter-grid-formatter"; +import CustomActions from "../commons/custom-actions.js"; + + +export default class VariantTableFormatter { + + // DEPRECATED: use new consequenceTypes.impact instead + static assignColors(consequenceTypes, proteinSubstitutionScores) { + let result = {}; + if (consequenceTypes) { + const consequenceTypeToColor = {}; + const consequenceTypeToImpact = {}; + for (const category of consequenceTypes.categories) { + if (category.terms) { + for (const term of category.terms) { + consequenceTypeToColor[term.name] = consequenceTypes.style[term.impact]; + consequenceTypeToImpact[term.name] = term.impact; + } + } else { + if (category.id && category.name) { + consequenceTypeToColor[category.name] = consequenceTypes[category.impact]; + consequenceTypeToImpact[category.name] = category.impact; + } + } + } + result = { + consequenceTypeToColor: consequenceTypeToColor, + consequenceTypeToImpact: consequenceTypeToImpact + }; + } + + if (proteinSubstitutionScores) { + const pssColor = new Map(); + for (const i in proteinSubstitutionScores) { + if (Object.prototype.hasOwnProperty.call(proteinSubstitutionScores, i)) { + + const obj = proteinSubstitutionScores[i]; + Object.keys(obj).forEach(key => { + pssColor.set(key, obj[key]); + }); + } + } + result.pssColor = pssColor; + } + return result; + } + + static variantIdFormatter(id, variant) { + let ref = variant.reference ? variant.reference : "-"; + let alt = variant.alternate ? variant.alternate : "-"; + + // Check size + const maxAlleleLength = 20; + ref = (ref.length > maxAlleleLength) ? ref.substring(0, 4) + "..." + ref.substring(ref.length - 4) : ref; + alt = (alt.length > maxAlleleLength) ? alt.substring(0, 4) + "..." + alt.substring(alt.length - 4) : alt; + + // Ww need to escape < and > symbols from , , ... + alt = alt.replaceAll("<", "<").replaceAll(">", ">"); + return `${variant.chromosome}:${variant.start} ${ref}/${alt}`; + } + + static variantFormatter() { + return { + title: "Variant ID", + field: "id", + type: "basic", + display: { + format: (id, variant) => { + const variantId = VariantTableFormatter.variantIdFormatter(id, variant); + const snpId = VariantTableFormatter.snpFormatter(id, variant); + if (snpId) { + return `${variantId} (${snpId}`; + } else { + return `${variantId}`; + } + }, + style: { + "font-weight": "bold", + } + }, + }; + } + + static snpFormatter(value, row) { + // We try first to read SNP ID from the 'names' of the variant (this identifier comes from the file). + // If this ID is not a "rs..." then we search the rs in the CellBase XRef annotations. + // This field is in annotation.xref when source: "dbSNP". + let snpId = ""; + if (row.names && row.names.length > 0) { + for (const name of row.names) { + if (name.startsWith("rs")) { + snpId = name; + break; + } + } + } else { + if (row.annotation) { + if (row.annotation.id && row.annotation.id.startsWith("rs")) { + snpId = row.annotation.id; + } else { + if (row.annotation.xrefs) { + for (const xref of row.annotation.xrefs) { + if (xref.source === "dbSNP") { + snpId = xref.id; + break; + } + } + } + } + } + } + + return snpId; + } + + static geneIdFormatter(variant) { + if (!variant.annotation) { + variant.annotation = { + consequenceTypes: [] + }; + } + // const {selectedConsequenceTypes, notSelectedConsequenceTypes} = + // VariantTableFormatter._consequenceTypeDetailFormatterFilter(variant.annotation.consequenceTypes, gridCtSettings); + + // Keep a map of genes and the SO accessions and names + // const geneHasQueryCt = new Set(); + // if (query?.ct) { + // const consequenceTypes = new Set(); + // for (const ct of query.ct.split(",")) { + // consequenceTypes.add(ct); + // } + // + // for (const ct of selectedConsequenceTypes) { + // if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { + // geneHasQueryCt.add(ct.geneName); + // } + // } + // } + + if (variant?.annotation?.consequenceTypes?.length > 0) { + const visited = {}; + const genes = []; + // const geneWithCtLinks = []; + for (let i = 0; i < variant.annotation.consequenceTypes.length; i++) { + const geneName = variant.annotation.consequenceTypes[i].geneName; + + // We process Genes just one time + if (geneName && !visited[geneName]) { + // let geneViewMenuLink = ""; + // if (opencgaSession.project && opencgaSession.study) { + // geneViewMenuLink = `
+ // Gene View + //
`; + // } + + // const tooltipText = ` + // ${geneViewMenuLink} + // ${this.getGeneTooltip(geneName, this.opencgaSession?.project?.organism?.assembly)} + // `; + + // If query.ct exists + // if (query?.ct) { + // // If gene contains one of the query.ct + // if (geneHasQueryCt.has(geneName)) { + // geneWithCtLinks.push(` + // ${geneName} + // `); + // } else { + // geneLinks.push(` + // ${geneName} + // `); + // } + // } else { + // // No query.ct passed + // geneLinks.push(` + // ${geneName} + // `); + // } + + genes.push(geneName); + visited[geneName] = true; + } + } + + // Do not write more than 4 genes per line, this could be easily configurable + // let resultHtml = ""; + // const maxDisplayedGenes = 10; + // const allGenes = geneWithCtLinks.concat(geneLinks); + + // if (allGenes.length <= maxDisplayedGenes) { + // resultHtml = allGenes.join(","); + // } else { + // resultHtml = ` + //
+ // ${allGenes.slice(0, maxDisplayedGenes).join(",")} + // + // ,${allGenes.slice(maxDisplayedGenes).join(",")} + // + // + //
+ // `; + // } + return genes.join(", "); + } else { + return "-"; + } + } + + static geneFormatter() { + return { + title: "Gene ID", + field: "id", + type: "basic", + display: { + format: (id, variant) => { + return VariantTableFormatter.geneIdFormatter(variant); + }, + // style: { + // "font-weight": "bold", + // } + }, + }; + } + + static getGeneTooltip(geneName, assembly) { + return ` + +
+ Ensembl +
+
+ LRG +
+
+ UniProt +
+
+ Varsome +
+ + +
+ Decipher +
+
+ COSMIC +
+
+ OMIM +
+ `; + } + + static hgvsFormatter(variant, gridConfig) { + BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); + const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(variant.annotation?.consequenceTypes, gridConfig).indexes; + + if (showArrayIndexes?.length > 0 && variant.annotation.hgvs?.length > 0) { + const results = []; + for (const index of showArrayIndexes) { + const consequenceType = variant.annotation.consequenceTypes[index]; + const hgvsTranscriptIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.transcriptId)); + const hgvsProteingIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.proteinVariantAnnotation?.proteinId)); + if (hgvsTranscriptIndex > -1 || hgvsProteingIndex > -1) { + results.push(` +
+ ${VariantGridFormatter.getHgvsLink(consequenceType.transcriptId, variant.annotation.hgvs) || "-"} +
+
+ ${VariantGridFormatter.getHgvsLink(consequenceType.proteinVariantAnnotation?.proteinId, variant.annotation.hgvs) || "-"} +
+ `); + } + } + return results.join("
"); + } + } + + static vcfFormatter(value, row, field, type = "INFO") { + if (type.toUpperCase() === "INFO") { + return row.studies[0].files[0].data[field]; + } else { + const index = row.studies[0].sampleDataKeys.findIndex(f => f === field); + return row.studies[0].samples[0].data[index]; + } + } + + static typeFormatter(value, row) { + if (row) { + let type = row.type; + let color = ""; + switch (row.type) { + case "SNP": // Deprecated + type = "SNV"; + color = "black"; + break; + case "INDEL": + case "CNV": // Deprecated + case "COPY_NUMBER": + case "COPY_NUMBER_GAIN": + case "COPY_NUMBER_LOSS": + case "MNV": + color = "darkorange"; + break; + case "SV": + case "INSERTION": + case "DELETION": + case "DUPLICATION": + case "TANDEM_DUPLICATION": + case "BREAKEND": + color = "red"; + break; + default: + color = "black"; + break; + } + return `${type}`; + } else { + return "-"; + } + } + + static consequenceTypeFormatter(value, row, ctQuery, gridCtSettings) { + if (row?.annotation && row.annotation.consequenceTypes?.length > 0) { + let {selectedConsequenceTypes, notSelectedConsequenceTypes, indexes} = + VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, gridCtSettings); + + // If CT is passed in the query then we must make and AND with the selected transcript by the user. + // This means that only the selectedConsequenceTypes that ARE ALSO IN THE CT QUERY are displayed. + if (ctQuery) { + const consequenceTypes = new Set(); + for (const ct of ctQuery.split(",")) { + consequenceTypes.add(ct); + } + + const newSelectedConsequenceTypes = []; + for (const ct of selectedConsequenceTypes) { + if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { + newSelectedConsequenceTypes.push(ct); + } else { + notSelectedConsequenceTypes.push(ct); + } + } + selectedConsequenceTypes = newSelectedConsequenceTypes; + } + + const positiveConsequenceTypes = []; + const negativeConsequenceTypes = []; + const soVisited = new Set(); + for (const ct of selectedConsequenceTypes) { + for (const so of ct.sequenceOntologyTerms) { + if (!soVisited.has(so?.name)) { + positiveConsequenceTypes.push(`${so.name}`); + soVisited.add(so.name); + } + } + } + + // Print negative SO, if not printed as positive + let negativeConsequenceTypesText = ""; + if (gridCtSettings.consequenceType.showNegativeConsequenceTypes) { + for (const ct of notSelectedConsequenceTypes) { + for (const so of ct.sequenceOntologyTerms) { + if (!soVisited.has(so.name)) { + negativeConsequenceTypes.push(`
${so.name}
`); + soVisited.add(so.name); + } + } + } + + if (negativeConsequenceTypes.length > 0) { + negativeConsequenceTypesText = ` + ${negativeConsequenceTypes.length} terms filtered + `; + } + } + + return ` +
+ ${positiveConsequenceTypes.join("
")} +
+
+ ${negativeConsequenceTypesText} +
`; + } + return "-"; + } + + /* Usage: + columns: [ + { + title: "", classes: "", style: "", + columns: [ // nested column + { + title: "", classes: "", style: "" + } + ] + } + ] + + rows: [ + {values: ["", ""], classes: "", style: ""} + ] + */ + static renderTable(id, columns, rows, config) { + if (!rows || rows.length === 0) { + return `${config?.defaultMessage ? config.defaultMessage : "No data found"}`; + } + + let tr = ""; + const nestedColumnIndex = columns.findIndex(col => col.columns?.length > 0); + if (nestedColumnIndex > -1) { + let thTop = ""; + let thBottom = ""; + for (const column of columns) { + if (column.columns?.length > 0) { + thTop += `${column.title}`; + for (const bottomColumn of column.columns) { + thBottom += `${bottomColumn.title}`; + } + } else { + thTop += `${column.title}`; + } + } + tr += `${thTop}`; + tr += `${thBottom}`; + } else { + const th = columns.map(column => `${column.title}`).join(""); + tr = `${th}`; + } + + let html = ` + + ${tr} + + `; + // Render rows + for (const row of rows) { + let td = ""; + for (const value of row.values) { + td += ``; + } + html += `${td}`; + } + html += "
${value}
"; + + return html; + } + + static _consequenceTypeDetailFormatterFilter(cts, filter) { + const selectedConsequenceTypes = []; + const notSelectedConsequenceTypes = []; + const showArrayIndexes = []; + + const geneSet = filter?.geneSet ? filter.geneSet : {}; + for (let i = 0; i < cts.length; i++) { + const ct = cts[i]; + + // Check if gene source is valid + let isSourceValid = false; + if (geneSet["ensembl"] && (!ct.source || ct.source.toUpperCase() === "ENSEMBL")) { // FIXME: Ensembl regulatory CT do not have 'source' + isSourceValid = true; + } else { + if (geneSet["refseq"] && ct.source?.toUpperCase() === "REFSEQ") { + isSourceValid = true; + } + } + if (!isSourceValid) { + // Not a valid source, let's continue to next ct + continue; + } + + // TODO Remove in IVA 2.3 + // To keep compatability with CellBase 4 + const transcriptFlags = ct.transcriptFlags ?? ct.transcriptAnnotationFlags; + let isCtSelected = filter.consequenceType?.all || false; + if (filter && isCtSelected === false) { + if (filter.consequenceType.maneTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("MANE Select")|| transcriptFlags?.includes("MANE Plus Clinical"); + } + if (filter.consequenceType.ensemblCanonicalTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("canonical"); + } + if (filter.consequenceType.gencodeBasicTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("basic"); + } + if (filter.consequenceType.ccdsTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("CCDS"); + } + if (filter.consequenceType.lrgTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("LRG"); + } + if (filter.consequenceType.ensemblTslTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("TSL:1"); + } + if (filter.consequenceType.illuminaTSO500Transcript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("TSO500"); + } + if (filter.consequenceType.eglhHaemoncTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("EGLH_HaemOnc"); + } + if (filter.consequenceType.proteinCodingTranscript && ct.biotype === "protein_coding") { + isCtSelected = isCtSelected || ct.biotype === "protein_coding"; + } + if (filter.consequenceType.highImpactConsequenceTypeTranscript) { + for (const so of ct.sequenceOntologyTerms) { + const impact = CONSEQUENCE_TYPES?.impact[so.name]?.toUpperCase(); + isCtSelected = isCtSelected || impact === "HIGH" || impact === "MODERATE"; + } + } + } + // Check if the CT satisfy any condition + if (isCtSelected) { + showArrayIndexes.push(i); + selectedConsequenceTypes.push(ct); + } else { + notSelectedConsequenceTypes.push(ct); + } + } + return { + selectedConsequenceTypes: selectedConsequenceTypes, + notSelectedConsequenceTypes: notSelectedConsequenceTypes, + indexes: showArrayIndexes + }; + } + + static getHgvsLink(id, hgvsArray) { + if (!id) { + return; + } + + let hgvs = hgvsArray?.find(hgvs => hgvs.startsWith(id)); + if (hgvs) { + if (hgvs.includes("(")) { + const split = hgvs.split(new RegExp("[()]")); + hgvs = split[0] + split[2]; + } + + const split = hgvs.split(":"); + let link; + if (hgvs.includes(":c.")) { + link = BioinfoUtils.getTranscriptLink(split[0]); + } + if (hgvs.includes(":p.")) { + link = BioinfoUtils.getProteinLink(split[0]); + } + + return `${split[0]}:${split[1]}`; + } else { + if (id.startsWith("ENST") || id.startsWith("NM_") || id.startsWith("NR_")) { + return `${id}`; + } else { + return `${id}`; + } + } + } + + static toggleDetailConsequenceType(e) { + const id = e.target.dataset.id; + const elements = document.getElementsByClassName(this._prefix + id + "Filtered"); + for (const element of elements) { + if (element.style.display === "none") { + element.style.display = ""; + } else { + element.style.display = "none"; + } + } + } + + static consequenceTypeDetailFormatter(value, row, variantGrid, query, filter, assembly) { + if (row?.annotation?.consequenceTypes && row.annotation.consequenceTypes.length > 0) { + // Sort and group CTs by Gene name + BioinfoUtils.sort(row.annotation.consequenceTypes, v => v.geneName); + + const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, filter).indexes; + let message = ""; + if (filter) { + // Create two different divs to 'show all' or 'apply filter' title + message = `
+ Showing ${showArrayIndexes.length} of + ${row.annotation.consequenceTypes.length} consequence types, + show all... +
+ + `; + } + + let ctHtml = `
+ ${message} +
+ + + + + + + + + + + + + + + + + + + + + + `; + + for (let i = 0; i < row.annotation.consequenceTypes.length; i++) { + const ct = row.annotation.consequenceTypes[i]; + + // Keep backward compatibility with old ensemblGeneId and ensemblTranscriptId + const source = ct.source || "ensembl"; + const geneId = ct.geneId || ct.ensemblGeneId; + const transcriptId = ct.transcriptId || ct.ensemblTranscriptId; + const geneIdLink = `${BioinfoUtils.getGeneLink(geneId, source, assembly)}`; + const ensemblTranscriptIdLink = `${BioinfoUtils.getTranscriptLink(transcriptId, source, assembly)}`; + + // Prepare data info for columns + const geneName = ct.geneName ? `${ct.geneName}` : "-"; + + const geneIdLinkHtml = geneId ? `${geneId}` : ""; + const geneHtml = ` +
${geneName}
+
${geneIdLinkHtml}
+ `; + + const transcriptIdHtml = ` +
+ ${ct.biotype ? ct.biotype : "-"} +
+
+ + ${transcriptId ? ` +
+ ${VariantGridFormatter.getHgvsLink(transcriptId, row.annotation.hgvs) || ""} +
+
+ ${VariantGridFormatter.getHgvsLink(ct?.proteinVariantAnnotation?.proteinId, row.annotation.hgvs) || ""} +
` : "" + } +
+
`; + + const soArray = []; + for (const so of ct.sequenceOntologyTerms) { + const color = CONSEQUENCE_TYPES.style[CONSEQUENCE_TYPES.impact[so.name]] || "black"; + const soUrl = `${BioinfoUtils.getSequenceOntologyLink(so.accession)}`; + const soTitle = `Go to Sequence Ontology ${so.accession} term`; + soArray.push(` +
+ ${so.name} + + + +
+ `); + } + + let transcriptFlags = ["-"]; + if (transcriptId && (ct.transcriptFlags?.length > 0 || ct.transcriptAnnotationFlags?.length > 0)) { + transcriptFlags = ct.transcriptFlags ? + ct.transcriptFlags.map(flag => `
${flag}
`) : + ct.transcriptAnnotationFlags.map(flag => `
${flag}
`); + } + + let exons = ["-"]; + if (ct.exonOverlap && ct.exonOverlap.length > 0) { + exons = ct.exonOverlap.map(exon => ` +
+ ${exon.number} +
+ ${exon?.percentage ? ` +
+ ${exon?.percentage.toFixed(2) ?? "-"}% +
` : + ""} + `); + } + + let spliceAIScore = "-"; + if (ct.spliceScores?.length > 0) { + const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); + if (spliceAi) { + const keys = ["DS_AG", "DS_AL", "DS_DG", "DS_DL"]; + const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); + const index = [spliceAi.scores[keys[0]], spliceAi.scores[keys[1]], spliceAi.scores[keys[2]], spliceAi.scores[keys[3]]].findIndex(e => e === max); + + const color = (max >= 0.8) ? "red" : (max >= 0.5) ? "darkorange" : "black"; + spliceAIScore = ` +
+ ${max} (${keys[index]}) +
+ `; + } + } + + const pva = ct.proteinVariantAnnotation ? ct.proteinVariantAnnotation : {}; + let uniprotAccession = "-"; + if (pva.uniprotAccession) { + uniprotAccession = ` + + ${pva.uniprotVariantId ? ` +
+ ${pva.uniprotVariantId} +
` : + ""} + `; + } + + let domains = ` + + `; + if (pva.features) { + let tooltipText = ""; + const visited = new Set(); + for (const feature of pva.features) { + if (feature.id && !visited.has(feature.id)) { + visited.add(feature.id); + tooltipText += ` +
+ ${feature.id}${feature.description} +
+ `; + } + } + domains = ` + + `; + } + + // Create the table row + const hideClass = showArrayIndexes.includes(i) ? "" : `${variantGrid._prefix}${row.id}Filtered`; + const displayStyle = showArrayIndexes.includes(i) ? "" : "display: none"; + ctHtml += ` + + + + + + + + + + + + + + + `; + } + ctHtml += "
GeneTranscriptConsequence TypeTranscript FlagsTranscript Variant AnnotationProtein Variant Annotation
cDNA / CDSCodonExon (%)SpliceAIUniProt AccPositionRef/AltDomains
${geneHtml}${transcriptIdHtml}${soArray.join("")}${transcriptFlags.join("")}${ct.cdnaPosition || "-"} / ${ct.cdsPosition || "-"}${ct.codon || "-"}${exons.join("
")}
${spliceAIScore}${uniprotAccession}${pva.position !== undefined ? pva.position : "-"}${pva.reference !== undefined ? pva.reference + "/" + pva.alternate : "-"}${domains}
"; + return ctHtml; + } + return "-"; + } + + static caddScaledFormatter(value, row, index) { + if (row && row.type !== "INDEL" && row.annotation?.functionalScore?.length > 0) { + for (const functionalScore of row.annotation.functionalScore) { + if (functionalScore.source === "cadd_scaled") { + const value = Number(functionalScore.score).toFixed(2); + if (value < 15) { + return value; + } else { + return "" + value + ""; + } + } + } + } else { + return "-"; + } + } + + static spliceAIFormatter(value, row) { + if (row.annotation.consequenceTypes?.length > 0) { + // We need to find the max Delta Score: + // Delta score of a variant, defined as the maximum of (DS_AG, DS_AL, DS_DG, DS_DL), + // ranges from 0 to 1 and can be interpreted as the probability of the variant being splice-altering. + let dscore = 0; + let transcriptId; + for (const ct of row.annotation.consequenceTypes) { + if (ct.spliceScores?.length > 0) { + const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); + if (spliceAi) { + const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); + if (max > dscore) { + dscore = max; + transcriptId = ct.transcriptId; + } + } + } + } + + const color = (dscore >= 0.8) ? "red" : (dscore >= 0.5) ? "darkorange" : "black"; + return ` +
+ ${dscore} +
+ `; + } else { + return "-"; + } + } + + static populationFrequenciesInfoTooltipContent(populationFrequencies) { + return `One coloured square is shown for each population. Frequencies are coded with colours which classify values + into 'very rare', 'rare', 'average', 'common' or 'missing', see + + https://www.nature.com/scitable/topicpage/multifactorial-inheritance-and-genetic-disease-919 + . Please, leave the cursor over each square to display the actual frequency values.
+ Note that that all frequencies are percentages. +
+
Very rare: freq < 0.1 %
+
Rare: freq < 0.5 %
+
Average: freq < 5 %
+
Common: freq >= 5 %
+
Not observed
`; + } + + // Creates the colored table with one row and as many columns as populations. + static renderPopulationFrequencies(populations, populationFrequenciesMap, populationFrequenciesColor, populationFrequenciesConfig = {displayMode: "FREQUENCY_BOX"}) { + const tooltipRows = (populations || []).map(population => { + const popFreq = populationFrequenciesMap.get(population) || null; + const altFreq = popFreq?.altAlleleFreq?.toPrecision(4) || 0; + const altCount = popFreq?.altAlleleCount || 0; + const homAltFreq = popFreq?.altHomGenotypeFreq?.toPrecision(4) || 0; + const homAltCount = popFreq?.altHomGenotypeCount || 0; + const color = VariantGridFormatter._getPopulationFrequencyColor(altFreq, populationFrequenciesColor); + let altFreqText = ""; + let homAltFreqText = ""; + + // ALT freq tell us if the VARIANT has been OBSERVED. + if (altFreq > 0) { + altFreqText = `${altFreq || "-"} / ${altCount} (${altFreq > 0 ? (altFreq * 100).toPrecision(4) + "%" : "-"})`; + homAltFreqText = `${homAltFreq > 0 ? homAltFreq : "-"} / ${homAltCount} ${homAltFreq > 0 ? `(${(homAltFreq * 100).toPrecision(4)} %)` : ""}`; + } else { + altFreqText = "Not Observed"; + homAltFreqText = "Not Observed"; + } + + return ` + + + + + + ${altFreqText} + ${homAltFreqText} + + `; + }); + const tooltip = ` + + + + + + + + + ${tooltipRows.join("")} +
PopulationAllele ALT
(freq/count)
Genotype HOM_ALT
(freq/count)
+ `; + + // Create the table (with the tooltip info) + let htmlPopFreqTable; + if (populationFrequenciesConfig?.displayMode === "FREQUENCY_BOX") { + const tableSize = populations.length * 15; + htmlPopFreqTable = ` + + + + `; + for (const population of populations) { + // This array contains "study:population" + let color = "black"; + if (typeof populationFrequenciesMap.get(population) !== "undefined") { + const freq = populationFrequenciesMap.get(population).altAlleleFreq || 0; + color = VariantGridFormatter._getPopulationFrequencyColor(freq, populationFrequenciesColor); + } + htmlPopFreqTable += ``; + } + htmlPopFreqTable += "
 
"; + } else { + htmlPopFreqTable = "
"; + const populationFrequenciesHtml = []; + for (const population of populations) { + let color = "black"; + if (typeof populationFrequenciesMap.get(population) !== "undefined") { // Freq exists + const freq = populationFrequenciesMap.get(population).altAlleleFreq || 0; + const percentage = (Number(freq) * 100).toPrecision(4); + // Only color the significant ones + if (freq <= 0.005) { + color = VariantGridFormatter._getPopulationFrequencyColor(freq, populationFrequenciesColor); + } + + if (populations.length > 1) { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`${population}`); + populationFrequenciesHtml.push(`${freq}`); + populationFrequenciesHtml.push(`(${percentage} %)`); + populationFrequenciesHtml.push("
"); + } else { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`${freq}`); + populationFrequenciesHtml.push(`(${percentage} %)`); + populationFrequenciesHtml.push("
"); + } + } else { // Freq does not exist + if (populations.length > 1) { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`${population}`); + populationFrequenciesHtml.push(`NA`); + populationFrequenciesHtml.push("
"); + } else { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`NA`); + populationFrequenciesHtml.push("
"); + } + } + } + htmlPopFreqTable += `${populationFrequenciesHtml.join("")}`; + htmlPopFreqTable += "
"; + } + + return htmlPopFreqTable; + } + + static _getPopulationFrequencyColor(freq, populationFrequenciesColor) { + let color; + if (freq === 0 || freq === "0") { + color = populationFrequenciesColor.unobserved; + } else if (freq < 0.001) { + color = populationFrequenciesColor.veryRare; + } else if (freq < 0.005) { + color = populationFrequenciesColor.rare; + } else if (freq < 0.05) { + color = populationFrequenciesColor.average; + } else { + color = populationFrequenciesColor.common; + } + return color; + } + + static clinicalTraitAssociationFormatter(value, row, index) { + const phenotypeHtml = ""; + // Check for ClinVar and Cosmic annotations + if (row?.annotation?.traitAssociation) { + // Filter the traits for this column and check the number of existing traits + const traits = row.annotation.traitAssociation.filter(trait => trait.source.name.toUpperCase() === this.field.toUpperCase()); + if (traits.length === 0) { + return ""; + } + + let tooltipText = ""; + switch (this.field) { + case "clinvar": + const results = []; + const clinicalSignificanceVisited = new Set(); + for (const trait of traits) { + let clinicalSignificance, + drugResponseClassification; + if (trait?.variantClassification?.clinicalSignificance) { + clinicalSignificance = trait.variantClassification.clinicalSignificance; + } else { + if (trait?.variantClassification?.drugResponseClassification) { + clinicalSignificance = "drug_response"; + drugResponseClassification = trait?.variantClassification?.drugResponseClassification; + } else { + clinicalSignificance = "unknown"; + } + } + let code = ""; + let color = ""; + let tooltip = ""; + switch (clinicalSignificance.toUpperCase()) { + case "BENIGN": + code = CLINICAL_SIGNIFICANCE_SETTINGS.BENIGN.id; + color = "green"; + tooltip = "Classified as benign following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "LIKELY_BENIGN": + code = CLINICAL_SIGNIFICANCE_SETTINGS.LIKELY_BENIGN.id; + color = "darkgreen"; + tooltip = "Classified as likely benign following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "VUS": + case "UNCERTAIN_SIGNIFICANCE": + code = CLINICAL_SIGNIFICANCE_SETTINGS.UNCERTAIN_SIGNIFICANCE.id; + color = "darkorange"; + tooltip = "Classified as of uncertain significance following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "LIKELY_PATHOGENIC": + code = CLINICAL_SIGNIFICANCE_SETTINGS.LIKELY_PATHOGENIC.id; + color = "darkred"; + tooltip = "Classified as likely pathogenic following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "PATHOGENIC": + code = CLINICAL_SIGNIFICANCE_SETTINGS.PATHOGENIC.id; + color = "red"; + tooltip = "Classified as pathogenic following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "DRUG_RESPONSE": + code = "DR"; + color = "darkred"; + tooltip = "Classified as drug response following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "UNKNOWN": + code = "NP"; + color = "grey"; + tooltip = "ClinVar submissions without an interpretation of clinical significance"; + break; + } + + if (code !== "NP" && !clinicalSignificanceVisited.has(code)) { + results.push(`${code}`); + clinicalSignificanceVisited.add(code); + } + + // Prepare the tooltip links + if (!trait.id?.startsWith("SCV")) { + // We display the link plus the clinical significance and all the heritable trait descriptions + tooltipText += ` +
+
+ ${trait.id} + + ${clinicalSignificance} ${drugResponseClassification ? "(" + drugResponseClassification + ")" : ""} + +
+
+ ${trait?.heritableTraits?.length > 0 && trait.heritableTraits + .filter(t => t.trait && t.trait !== "not specified" && t.trait !== "not provided") + .map(t => `${t.trait}`) + .join("") + } +
+
`; + } + } + + // This can only be shown if nothing else exists + if (results.length === 0) { + return "NP"; + } + + return `${results.join("
")}
`; + case "cosmic": + // Prepare the tooltip links + const cosmicMap = new Map(); + traits.forEach(trait => { + if (!cosmicMap.has(trait.id)) { + cosmicMap.set(trait.id, new Set()); + } + if (trait?.somaticInformation?.primaryHistology) { + cosmicMap.get(trait.id).add(trait.somaticInformation.primaryHistology); + } + }); + + Array.from(cosmicMap.entries()).forEach(([traitId, histologies]) => { + const histologiesItems = Array.from(histologies.values()) + .filter(histology => histology && histology !== "null") + .map(histology => `${histology}`) + .join(""); + + tooltipText += ` +
+ +
+ ${histologiesItems} +
+
+ `; + }); + + return ` + + ${cosmicMap.size} ${cosmicMap.size > 1 ? "studies" : "study" } + `; + default: + console.error("Wrong clinical source : " + this.field); + break; + } + } + return phenotypeHtml; + } + + static clinicalCancerHotspotsFormatter(value, row) { + if (row?.annotation?.cancerHotspots?.length > 0) { + const cancerHotspotsHtml = new Map(); + for (const ct of row.annotation.consequenceTypes) { + for (const hotspot of row.annotation.cancerHotspots) { + if (ct.geneName === hotspot.geneName && ct.proteinVariantAnnotation?.position === hotspot.aminoacidPosition) { + cancerHotspotsHtml.set(`${hotspot.geneName}_${hotspot.aminoacidPosition}`, hotspot); + } + } + } + let tooltipText = ""; + for (const [key, hotspot] of cancerHotspotsHtml.entries()) { + tooltipText += ` +
+
+ +
Cancer Type: ${hotspot.cancerType} - ${hotspot.variants.length} ${hotspot.variants.length === 1 ? "mutation" : "mutations"}
+
+
+ ${ + hotspot.variants + .map(variant => ` + ${AMINOACID_CODE[hotspot.aminoacidReference]}${hotspot.aminoacidPosition}${AMINOACID_CODE[variant.aminoacidAlternate]}: ${variant.count} sample(s) + `) + .join("") + } +
+
`; + } + + if (cancerHotspotsHtml.size > 0) { + return ` + + ${cancerHotspotsHtml.size} ${cancerHotspotsHtml.size === 1 ? "variant" : "variants"} + `; + } + } + return ""; + } + + static clinicalTableDetail(value, row, index) { + const clinvar = []; + const cosmic = []; + const hotspots = []; + if (row.annotation?.traitAssociation?.length > 0) { + const cosmicIntermediate = new Map(); + for (const trait of row.annotation.traitAssociation) { + const values = []; + const vcvId = trait.additionalProperties.find(p => p.name === "VCV ID"); + const genomicFeature = trait.genomicFeatures.find(f => f.featureType.toUpperCase() === "GENE"); + const reviewStatus = trait.additionalProperties.find(p => p.name === "ReviewStatus_in_source_file"); + if (trait.source.name.toUpperCase() === "CLINVAR") { + values.push(`${trait.id}`); + values.push(vcvId ? vcvId.value : trait.id); + values.push(genomicFeature?.xrefs ? genomicFeature.xrefs?.symbol : "-"); + values.push(trait.variantClassification?.clinicalSignificance); + values.push(trait.consistencyStatus); + values.push(reviewStatus ? reviewStatus.value : "-"); + values.push(trait.heritableTraits ? trait.heritableTraits.map(t => t.trait).join("
") : "-"); + clinvar.push({ + values: values + }); + } else { // COSMIC section + // Prepare data to group by histologySubtype field + const key = trait.id + ":" + trait.somaticInformation.primaryHistology + ":" + trait.somaticInformation.primaryHistology; + const reviewStatus = trait.additionalProperties.find(p => p.id === "MUTATION_SOMATIC_STATUS"); + const zygosity = trait.additionalProperties.find(p => p.id === "MUTATION_ZYGOSITY"); + if (!cosmicIntermediate.has(key)) { + cosmicIntermediate.set(key, { + id: trait.id, + url: trait.url, + primarySite: trait.somaticInformation.primarySite, + primaryHistology: trait.somaticInformation.primaryHistology, + histologySubtypes: [], + histologySubtypesCounter: new Map(), + reviewStatus: reviewStatus, + pubmed: new Set(), + zygosity: new Set() + }); + } + // Only add the new terms for this key + if (trait.somaticInformation.histologySubtype) { + if (!cosmicIntermediate.get(key).histologySubtypesCounter.get(trait.somaticInformation.histologySubtype)) { + cosmicIntermediate.get(key).histologySubtypes.push(trait.somaticInformation.histologySubtype); + } + // Increment the counter always + cosmicIntermediate.get(key).histologySubtypesCounter + .set(trait.somaticInformation.histologySubtype, cosmicIntermediate.get(key).histologySubtypesCounter.size + 1); + } + if (trait?.bibliography?.length > 0) { + cosmicIntermediate.get(key).pubmed.add(...trait.bibliography); + } + if (zygosity) { + cosmicIntermediate.get(key).zygosity.add(zygosity.value); + } + } + } + + // Sort by key and prepare column data + for (const [key, c] of new Map([...cosmicIntermediate.entries()].sort())) { + const values = []; + values.push(`${c.id}`); + values.push(c.primarySite); + values.push(c.primaryHistology); + values.push(c.histologySubtypes + .map(value => { + if (cosmicIntermediate.get(key).histologySubtypesCounter.get(value) > 1) { + return value + " (x" + cosmicIntermediate.get(key).histologySubtypesCounter.get(value) + ")"; + } else { + return "-"; + } + }) + .join("
") || "-"); + values.push(Array.from(c.zygosity?.values()).join(", ") || "-"); + values.push(c?.reviewStatus?.value || "-"); + values.push(Array.from(c.pubmed.values()).map(p => `${p}`).join("
")); + cosmic.push({ + values: values + }); + } + } + + if (row?.annotation?.cancerHotspots?.length > 0) { + const visited = {}; + for (const ct of row.annotation.consequenceTypes) { + for (const hotspot of row.annotation.cancerHotspots) { + if (ct.geneName === hotspot.geneName && ct.proteinVariantAnnotation?.position === hotspot.aminoacidPosition && !visited[hotspot.geneName + "_" + hotspot.aminoacidPosition]) { + const reference = AMINOACID_CODE[hotspot.aminoacidReference]; + const position = hotspot.aminoacidPosition; + const values = []; + values.push(hotspot.geneName); + values.push(reference); + values.push(hotspot.aminoacidPosition); + values.push(hotspot.cancerType); + values.push(hotspot.variants.length); + values.push(hotspot.variants.map(m => `${reference}${position}${AMINOACID_CODE[m.aminoacidAlternate]}: ${m.count} sample(s)`).join("; ")); + hotspots.push({ + values: values + }); + visited[hotspot.geneName + "_" + hotspot.aminoacidPosition] = true; + } + } + } + } + + // Clinvar + const clinvarColumns = [ + {title: "ID"}, + {title: "Variation ID"}, + {title: "Gene"}, + {title: "Clinical Significance"}, + {title: "Consistency Status"}, + {title: "Review Status"}, + {title: "Traits"} + ]; + const clinvarTable = VariantGridFormatter.renderTable("", clinvarColumns, clinvar, {defaultMessage: "No ClinVar data found"}); + const clinvarTraits = ` +
+ +
${clinvarTable}
+
`; + + // Cosmic + const cosmicColumns = [ + {title: "ID"}, + {title: "Primary Site"}, + {title: "Primary Histology"}, + {title: "Histology Subtype"}, + {title: "Zygosity"}, + {title: "Status"}, + {title: "Pubmed"} + ]; + const cosmicTable = VariantGridFormatter.renderTable("", cosmicColumns, cosmic, {defaultMessage: "No Cosmic data found"}); + const cosmicTraits = ` +
+ +
${cosmicTable}
+
`; + + // Cancer Hotspots + const cancerHotspotsColumns = [ + {title: "Gene Name"}, + {title: "Aminoacid Reference"}, + {title: "Aminoacid Position"}, + {title: "Cancer Type"}, + {title: "Number of Mutations"}, + {title: "Mutations"}, + ]; + const cancerHotspotsTable = VariantGridFormatter.renderTable("", cancerHotspotsColumns, hotspots, {defaultMessage: "No Cancer Hotspots data found"}); + const cancerHotspotsHtml = ` +
+ +
${cancerHotspotsTable}
+
`; + + return clinvarTraits + cosmicTraits + cancerHotspotsHtml; + } + + /* + * Reported Variant formatters + */ + static toggleDetailClinicalEvidence(e) { + const id = e.target.dataset.id; + const elements = document.getElementsByClassName(this._prefix + id + "EvidenceFiltered"); + for (const element of elements) { + if (element.style.display === "none") { + element.style.display = ""; + } else { + element.style.display = "none"; + } + } + } + + static reportedVariantFormatter(value, variant, index) { + return ` + ${variant?.interpretations?.length > 0 ? ` +
${variant.interpretations.length === 1 ? "1 case found" : `${variant.interpretations.length} cases found`}
+
+
REPORTED: ${variant.interpretationStats?.status?.REPORTED || 0} times
+
TIER 1: ${variant.interpretationStats?.tier?.TIER1 || 0} times
+
DISCARDED: ${variant.interpretationStats?.status?.DISCARDED || 0} times
+
` : ` +
No cases found
` + } + `; + } + + static reportedVariantDetailFormatter(value, row, opencgaSession) { + if (row?.interpretations?.length > 0) { + let reportedHtml = ` + + + + + + + + + + + + + + + + + + + + `; + + for (const interpretation of row.interpretations) { + // Prepare data info for columns + const caseId = interpretation.id.split(".")[0]; + const interpretationIdHtml = ` +
+ +
+ `; + + const panelsHtml = ` +
+ ${interpretation.panels?.map(panel => { + if (panel?.source?.project === "PanelApp") { + return `${panel.name}`; + } else { + return `${panel.name || "-"}`; + } + })?.join("
")} +
`; + + const interpretedVariant = interpretation.primaryFindings.find(variant => variant.id === row.id); + + const sampleHtml = ` +
+ +
`; + + const genotype = VariantInterpreterGridFormatter.alleleGenotypeRenderer(row, interpretedVariant.studies[0]?.samples[0], "call"); + const genotypeHtml = ` +
+ ${genotype || "-"} +
`; + + const statusHtml = ` +
+ ${interpretedVariant.status || "-"} +
`; + const discussionHtml = ` +
+ ${interpretedVariant?.discussion?.text || "-"} +
`; + + const genes = []; + const transcripts = []; + const acmgClassifications = []; + const tierClassifications = []; + const clinicalSignificances = []; + for (const evidence of interpretedVariant.evidences.filter(ev => ev.review.select)) { + genes.push(` + + ${evidence.genomicFeature.geneName} + + `); + transcripts.push(` + + ${evidence.genomicFeature.transcriptId} + + `); + acmgClassifications.push(evidence.review.acmg?.map(acmg => acmg.classification)?.join(", ")|| "-"); + tierClassifications.push(evidence.review.tier || "-"); + clinicalSignificances.push(` + + ${evidence.review.clinicalSignificance || "-"} + + `); + } + + // Create the table row + reportedHtml += ` + + + + + + + + + + + + + + + `; + } + reportedHtml += "
CaseDisease PanelSampleGenotypeStatusDiscussionEvidences
GeneTranscriptACMGTierClinical Significance
${interpretationIdHtml}${interpretation?.panels?.length > 0 ? panelsHtml : "-"}${sampleHtml}${genotypeHtml}${statusHtml}${discussionHtml}${genes.length > 0 ? genes.join("
") : "-"}
${transcripts.length > 0 ? transcripts.join("
") : "-"}
${acmgClassifications.length > 0 ? acmgClassifications.join("
") : "-"}
${tierClassifications.length > 0 ? tierClassifications.join("
") : "-"}
${clinicalSignificances.length > 0 ? clinicalSignificances.join("
") : "-"}
"; + return reportedHtml; + } + return "-"; + } + +} From e1b9f1689f01810b32fb9041d4aa29c3c96c858b Mon Sep 17 00:00:00 2001 From: imedina Date: Thu, 9 Nov 2023 11:48:39 +0000 Subject: [PATCH 027/153] wc: variant-formatter class added --- .../variant-browser-grid-test.js | 2 +- .../clinical/rga/rga-individual-family.js | 2 +- .../clinical/rga/rga-variant-allele-pairs.js | 2 +- .../clinical/rga/rga-variant-view.js | 2 +- .../variant-interpreter-grid.js | 2 +- .../variant-interpreter-rearrangement-grid.js | 4 +- .../variant/variant-browser-grid.js | 2 +- .../variant/variant-formatter.js | 1263 +++++++++++++++++ .../variant/variant-grid-formatter.js | 131 +- .../variant/variant-table-formatter.js | 201 +-- 10 files changed, 1364 insertions(+), 247 deletions(-) create mode 100644 src/webcomponents/variant/variant-formatter.js diff --git a/src/sites/test-app/webcomponents/variant-browser-grid-test.js b/src/sites/test-app/webcomponents/variant-browser-grid-test.js index 47c9e5d917..91675f9172 100644 --- a/src/sites/test-app/webcomponents/variant-browser-grid-test.js +++ b/src/sites/test-app/webcomponents/variant-browser-grid-test.js @@ -186,7 +186,7 @@ class VariantBrowserGridTest extends LitElement { // }), defaultValue: "-", columns: [ - VariantTableFormatter.variantFormatter(), + VariantTableFormatter.variantIdFormatter(), VariantTableFormatter.geneFormatter() // { // title: "Somatic", diff --git a/src/webcomponents/clinical/rga/rga-individual-family.js b/src/webcomponents/clinical/rga/rga-individual-family.js index 67feb09c82..6c805a4798 100644 --- a/src/webcomponents/clinical/rga/rga-individual-family.js +++ b/src/webcomponents/clinical/rga/rga-individual-family.js @@ -381,7 +381,7 @@ export default class RgaIndividualFamily extends LitElement { title: "Id", field: "id", rowspan: 2, - formatter: (value, row, index) => row.chromosome ? VariantGridFormatter.variantFormatter(value, row, index, this.opencgaSession.project.organism.assembly) : value + formatter: (value, row, index) => row.chromosome ? VariantGridFormatter.variantIdFormatter(value, row, index, this.opencgaSession.project.organism.assembly) : value }, { title: "Gene", diff --git a/src/webcomponents/clinical/rga/rga-variant-allele-pairs.js b/src/webcomponents/clinical/rga/rga-variant-allele-pairs.js index 5c237ffe72..372bb2ed58 100644 --- a/src/webcomponents/clinical/rga/rga-variant-allele-pairs.js +++ b/src/webcomponents/clinical/rga/rga-variant-allele-pairs.js @@ -233,7 +233,7 @@ export default class RgaVariantAllelePairs extends LitElement { { title: "Allele", field: "id", - formatter: (value, row, index) => row.chromosome ? VariantGridFormatter.variantFormatter(value, row, index, this.opencgaSession.project.organism.assembly) : value + formatter: (value, row, index) => row.chromosome ? VariantGridFormatter.variantIdFormatter(value, row, index, this.opencgaSession.project.organism.assembly) : value }, { title: "Pair type", diff --git a/src/webcomponents/clinical/rga/rga-variant-view.js b/src/webcomponents/clinical/rga/rga-variant-view.js index 4d9278ac5d..334661b118 100644 --- a/src/webcomponents/clinical/rga/rga-variant-view.js +++ b/src/webcomponents/clinical/rga/rga-variant-view.js @@ -205,7 +205,7 @@ export default class RgaVariantView extends LitElement { title: "Variant", field: "id", rowspan: 2, - formatter: (value, row, index) => VariantGridFormatter.variantFormatter(value, row, index, this.opencgaSession.project.organism.assembly) + formatter: (value, row, index) => VariantGridFormatter.variantIdFormatter(value, row, index, this.opencgaSession.project.organism.assembly) }, { title: "Gene", diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 7c4af986e7..31017b09e6 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -604,7 +604,7 @@ export default class VariantInterpreterGrid extends LitElement { field: "id", rowspan: 2, colspan: 1, - formatter: (value, row, index) => VariantGridFormatter.variantFormatter(value, row, index, this.opencgaSession.project.organism.assembly, this._config), + formatter: (value, row, index) => VariantGridFormatter.variantIdFormatter(value, row, index, this.opencgaSession.project.organism.assembly, this._config), halign: "center", // sortable: true visible: this.gridCommons.isColumnVisible("id"), diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 74242c7160..9d95c48a65 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -488,7 +488,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { field: "id", rowspan: 2, colspan: 1, - formatter: (value, row, index) => VariantGridFormatter.variantFormatter(value, row[0], index, this.opencgaSession.project.organism.assembly, this._config), + formatter: (value, row, index) => VariantGridFormatter.variantIdFormatter(value, row[0], index, this.opencgaSession.project.organism.assembly, this._config), halign: "center", sortable: true, visible: this.gridCommons.isColumnVisible("variant1"), @@ -499,7 +499,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { field: "id", rowspan: 2, colspan: 1, - formatter: (value, row, index) => VariantGridFormatter.variantFormatter(value, row[1], index, this.opencgaSession.project.organism.assembly, this._config), + formatter: (value, row, index) => VariantGridFormatter.variantIdFormatter(value, row[1], index, this.opencgaSession.project.organism.assembly, this._config), halign: "center", sortable: true, visible: this.gridCommons.isColumnVisible("variant2"), diff --git a/src/webcomponents/variant/variant-browser-grid.js b/src/webcomponents/variant/variant-browser-grid.js index 2f8d3457e8..c1a40641be 100644 --- a/src/webcomponents/variant/variant-browser-grid.js +++ b/src/webcomponents/variant/variant-browser-grid.js @@ -636,7 +636,7 @@ export default class VariantBrowserGrid extends LitElement { rowspan: 2, colspan: 1, formatter: (value, row, index) => - VariantGridFormatter.variantFormatter(value, row, index, this.opencgaSession.project.organism.assembly, this._config), + VariantGridFormatter.variantIdFormatter(value, row, index, this.opencgaSession.project.organism.assembly, this._config), halign: "center", visible: this.gridCommons.isColumnVisible("id") }, diff --git a/src/webcomponents/variant/variant-formatter.js b/src/webcomponents/variant/variant-formatter.js new file mode 100644 index 0000000000..08b33d5da2 --- /dev/null +++ b/src/webcomponents/variant/variant-formatter.js @@ -0,0 +1,1263 @@ +/* + * Copyright 2015-2016 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; +import VariantInterpreterGridFormatter from "./interpretation/variant-interpreter-grid-formatter"; + + +export default class VariantFormatter { + + static variantIdFormatter(id, variant) { + let ref = variant.reference ? variant.reference : "-"; + let alt = variant.alternate ? variant.alternate : "-"; + + // Check size + const maxAlleleLength = 20; + ref = (ref.length > maxAlleleLength) ? ref.substring(0, 4) + "..." + ref.substring(ref.length - 4) : ref; + alt = (alt.length > maxAlleleLength) ? alt.substring(0, 4) + "..." + alt.substring(alt.length - 4) : alt; + + // Ww need to escape < and > symbols from , , ... + alt = alt.replaceAll("<", "<").replaceAll(">", ">"); + return `${variant.chromosome}:${variant.start} ${ref}/${alt}`; + } + + static snpFormatter(value, row) { + // We try first to read SNP ID from the 'names' of the variant (this identifier comes from the file). + // If this ID is not a "rs..." then we search the rs in the CellBase XRef annotations. + // This field is in annotation.xref when source: "dbSNP". + let snpId = ""; + if (row.names && row.names.length > 0) { + for (const name of row.names) { + if (name.startsWith("rs")) { + snpId = name; + break; + } + } + } else { + if (row.annotation) { + if (row.annotation.id && row.annotation.id.startsWith("rs")) { + snpId = row.annotation.id; + } else { + if (row.annotation.xrefs) { + for (const xref of row.annotation.xrefs) { + if (xref.source === "dbSNP") { + snpId = xref.id; + break; + } + } + } + } + } + } + + return snpId; + } + + static hgvsFormatter(variant, gridConfig) { + BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); + const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(variant.annotation?.consequenceTypes, gridConfig).indexes; + + if (showArrayIndexes?.length > 0 && variant.annotation.hgvs?.length > 0) { + const results = []; + for (const index of showArrayIndexes) { + const consequenceType = variant.annotation.consequenceTypes[index]; + const hgvsTranscriptIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.transcriptId)); + const hgvsProteingIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.proteinVariantAnnotation?.proteinId)); + if (hgvsTranscriptIndex > -1 || hgvsProteingIndex > -1) { + results.push(` +
+ ${VariantGridFormatter.getHgvsLink(consequenceType.transcriptId, variant.annotation.hgvs) || "-"} +
+
+ ${VariantGridFormatter.getHgvsLink(consequenceType.proteinVariantAnnotation?.proteinId, variant.annotation.hgvs) || "-"} +
+ `); + } + } + return results.join("
"); + } + } + + static vcfFormatter(value, row, field, type = "INFO") { + if (type.toUpperCase() === "INFO") { + return row.studies[0].files[0].data[field]; + } else { + const index = row.studies[0].sampleDataKeys.findIndex(f => f === field); + return row.studies[0].samples[0].data[index]; + } + } + + static typeFormatter(value, row) { + if (row) { + let type = row.type; + let color = ""; + switch (row.type) { + case "SNP": // Deprecated + type = "SNV"; + color = "black"; + break; + case "INDEL": + case "CNV": // Deprecated + case "COPY_NUMBER": + case "COPY_NUMBER_GAIN": + case "COPY_NUMBER_LOSS": + case "MNV": + color = "darkorange"; + break; + case "SV": + case "INSERTION": + case "DELETION": + case "DUPLICATION": + case "TANDEM_DUPLICATION": + case "BREAKEND": + color = "red"; + break; + default: + color = "black"; + break; + } + return `${type}`; + } else { + return "-"; + } + } + + static consequenceTypeFormatter(value, row, ctQuery, gridCtSettings) { + if (row?.annotation && row.annotation.consequenceTypes?.length > 0) { + let {selectedConsequenceTypes, notSelectedConsequenceTypes, indexes} = + VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, gridCtSettings); + + // If CT is passed in the query then we must make and AND with the selected transcript by the user. + // This means that only the selectedConsequenceTypes that ARE ALSO IN THE CT QUERY are displayed. + if (ctQuery) { + const consequenceTypes = new Set(); + for (const ct of ctQuery.split(",")) { + consequenceTypes.add(ct); + } + + const newSelectedConsequenceTypes = []; + for (const ct of selectedConsequenceTypes) { + if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { + newSelectedConsequenceTypes.push(ct); + } else { + notSelectedConsequenceTypes.push(ct); + } + } + selectedConsequenceTypes = newSelectedConsequenceTypes; + } + + const positiveConsequenceTypes = []; + const negativeConsequenceTypes = []; + const soVisited = new Set(); + for (const ct of selectedConsequenceTypes) { + for (const so of ct.sequenceOntologyTerms) { + if (!soVisited.has(so?.name)) { + positiveConsequenceTypes.push(`${so.name}`); + soVisited.add(so.name); + } + } + } + + // Print negative SO, if not printed as positive + let negativeConsequenceTypesText = ""; + if (gridCtSettings.consequenceType.showNegativeConsequenceTypes) { + for (const ct of notSelectedConsequenceTypes) { + for (const so of ct.sequenceOntologyTerms) { + if (!soVisited.has(so.name)) { + negativeConsequenceTypes.push(`
${so.name}
`); + soVisited.add(so.name); + } + } + } + + if (negativeConsequenceTypes.length > 0) { + negativeConsequenceTypesText = ` + ${negativeConsequenceTypes.length} terms filtered + `; + } + } + + return ` +
+ ${positiveConsequenceTypes.join("
")} +
+
+ ${negativeConsequenceTypesText} +
`; + } + return "-"; + } + + /* Usage: + columns: [ + { + title: "", classes: "", style: "", + columns: [ // nested column + { + title: "", classes: "", style: "" + } + ] + } + ] + + rows: [ + {values: ["", ""], classes: "", style: ""} + ] + */ + static renderTable(id, columns, rows, config) { + if (!rows || rows.length === 0) { + return `${config?.defaultMessage ? config.defaultMessage : "No data found"}`; + } + + let tr = ""; + const nestedColumnIndex = columns.findIndex(col => col.columns?.length > 0); + if (nestedColumnIndex > -1) { + let thTop = ""; + let thBottom = ""; + for (const column of columns) { + if (column.columns?.length > 0) { + thTop += `${column.title}`; + for (const bottomColumn of column.columns) { + thBottom += `${bottomColumn.title}`; + } + } else { + thTop += `${column.title}`; + } + } + tr += `${thTop}`; + tr += `${thBottom}`; + } else { + const th = columns.map(column => `${column.title}`).join(""); + tr = `${th}`; + } + + let html = ` + + ${tr} + + `; + // Render rows + for (const row of rows) { + let td = ""; + for (const value of row.values) { + td += ``; + } + html += `${td}`; + } + html += "
${value}
"; + + return html; + } + + static _consequenceTypeDetailFormatterFilter(cts, filter) { + const selectedConsequenceTypes = []; + const notSelectedConsequenceTypes = []; + const showArrayIndexes = []; + + const geneSet = filter?.geneSet ? filter.geneSet : {}; + for (let i = 0; i < cts.length; i++) { + const ct = cts[i]; + + // Check if gene source is valid + let isSourceValid = false; + if (geneSet["ensembl"] && (!ct.source || ct.source.toUpperCase() === "ENSEMBL")) { // FIXME: Ensembl regulatory CT do not have 'source' + isSourceValid = true; + } else { + if (geneSet["refseq"] && ct.source?.toUpperCase() === "REFSEQ") { + isSourceValid = true; + } + } + if (!isSourceValid) { + // Not a valid source, let's continue to next ct + continue; + } + + // TODO Remove in IVA 2.3 + // To keep compatability with CellBase 4 + const transcriptFlags = ct.transcriptFlags ?? ct.transcriptAnnotationFlags; + let isCtSelected = filter.consequenceType?.all || false; + if (filter && isCtSelected === false) { + if (filter.consequenceType.maneTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("MANE Select")|| transcriptFlags?.includes("MANE Plus Clinical"); + } + if (filter.consequenceType.ensemblCanonicalTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("canonical"); + } + if (filter.consequenceType.gencodeBasicTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("basic"); + } + if (filter.consequenceType.ccdsTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("CCDS"); + } + if (filter.consequenceType.lrgTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("LRG"); + } + if (filter.consequenceType.ensemblTslTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("TSL:1"); + } + if (filter.consequenceType.illuminaTSO500Transcript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("TSO500"); + } + if (filter.consequenceType.eglhHaemoncTranscript) { + isCtSelected = isCtSelected || transcriptFlags?.includes("EGLH_HaemOnc"); + } + if (filter.consequenceType.proteinCodingTranscript && ct.biotype === "protein_coding") { + isCtSelected = isCtSelected || ct.biotype === "protein_coding"; + } + if (filter.consequenceType.highImpactConsequenceTypeTranscript) { + for (const so of ct.sequenceOntologyTerms) { + const impact = CONSEQUENCE_TYPES?.impact[so.name]?.toUpperCase(); + isCtSelected = isCtSelected || impact === "HIGH" || impact === "MODERATE"; + } + } + } + // Check if the CT satisfy any condition + if (isCtSelected) { + showArrayIndexes.push(i); + selectedConsequenceTypes.push(ct); + } else { + notSelectedConsequenceTypes.push(ct); + } + } + return { + selectedConsequenceTypes: selectedConsequenceTypes, + notSelectedConsequenceTypes: notSelectedConsequenceTypes, + indexes: showArrayIndexes + }; + } + + static getHgvsLink(id, hgvsArray) { + if (!id) { + return; + } + + let hgvs = hgvsArray?.find(hgvs => hgvs.startsWith(id)); + if (hgvs) { + if (hgvs.includes("(")) { + const split = hgvs.split(new RegExp("[()]")); + hgvs = split[0] + split[2]; + } + + const split = hgvs.split(":"); + let link; + if (hgvs.includes(":c.")) { + link = BioinfoUtils.getTranscriptLink(split[0]); + } + if (hgvs.includes(":p.")) { + link = BioinfoUtils.getProteinLink(split[0]); + } + + return `${split[0]}:${split[1]}`; + } else { + if (id.startsWith("ENST") || id.startsWith("NM_") || id.startsWith("NR_")) { + return `${id}`; + } else { + return `${id}`; + } + } + } + + static toggleDetailConsequenceType(e) { + const id = e.target.dataset.id; + const elements = document.getElementsByClassName(this._prefix + id + "Filtered"); + for (const element of elements) { + if (element.style.display === "none") { + element.style.display = ""; + } else { + element.style.display = "none"; + } + } + } + + static consequenceTypeDetailFormatter(value, row, variantGrid, query, filter, assembly) { + if (row?.annotation?.consequenceTypes && row.annotation.consequenceTypes.length > 0) { + // Sort and group CTs by Gene name + BioinfoUtils.sort(row.annotation.consequenceTypes, v => v.geneName); + + const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, filter).indexes; + let message = ""; + if (filter) { + // Create two different divs to 'show all' or 'apply filter' title + message = `
+ Showing ${showArrayIndexes.length} of + ${row.annotation.consequenceTypes.length} consequence types, + show all... +
+ + `; + } + + let ctHtml = `
+ ${message} +
+ + + + + + + + + + + + + + + + + + + + + + `; + + for (let i = 0; i < row.annotation.consequenceTypes.length; i++) { + const ct = row.annotation.consequenceTypes[i]; + + // Keep backward compatibility with old ensemblGeneId and ensemblTranscriptId + const source = ct.source || "ensembl"; + const geneId = ct.geneId || ct.ensemblGeneId; + const transcriptId = ct.transcriptId || ct.ensemblTranscriptId; + const geneIdLink = `${BioinfoUtils.getGeneLink(geneId, source, assembly)}`; + const ensemblTranscriptIdLink = `${BioinfoUtils.getTranscriptLink(transcriptId, source, assembly)}`; + + // Prepare data info for columns + const geneName = ct.geneName ? `${ct.geneName}` : "-"; + + const geneIdLinkHtml = geneId ? `${geneId}` : ""; + const geneHtml = ` +
${geneName}
+
${geneIdLinkHtml}
+ `; + + const transcriptIdHtml = ` +
+ ${ct.biotype ? ct.biotype : "-"} +
+
+ + ${transcriptId ? ` +
+ ${VariantGridFormatter.getHgvsLink(transcriptId, row.annotation.hgvs) || ""} +
+
+ ${VariantGridFormatter.getHgvsLink(ct?.proteinVariantAnnotation?.proteinId, row.annotation.hgvs) || ""} +
` : "" + } +
+
`; + + const soArray = []; + for (const so of ct.sequenceOntologyTerms) { + const color = CONSEQUENCE_TYPES.style[CONSEQUENCE_TYPES.impact[so.name]] || "black"; + const soUrl = `${BioinfoUtils.getSequenceOntologyLink(so.accession)}`; + const soTitle = `Go to Sequence Ontology ${so.accession} term`; + soArray.push(` +
+ ${so.name} + + + +
+ `); + } + + let transcriptFlags = ["-"]; + if (transcriptId && (ct.transcriptFlags?.length > 0 || ct.transcriptAnnotationFlags?.length > 0)) { + transcriptFlags = ct.transcriptFlags ? + ct.transcriptFlags.map(flag => `
${flag}
`) : + ct.transcriptAnnotationFlags.map(flag => `
${flag}
`); + } + + let exons = ["-"]; + if (ct.exonOverlap && ct.exonOverlap.length > 0) { + exons = ct.exonOverlap.map(exon => ` +
+ ${exon.number} +
+ ${exon?.percentage ? ` +
+ ${exon?.percentage.toFixed(2) ?? "-"}% +
` : + ""} + `); + } + + let spliceAIScore = "-"; + if (ct.spliceScores?.length > 0) { + const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); + if (spliceAi) { + const keys = ["DS_AG", "DS_AL", "DS_DG", "DS_DL"]; + const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); + const index = [spliceAi.scores[keys[0]], spliceAi.scores[keys[1]], spliceAi.scores[keys[2]], spliceAi.scores[keys[3]]].findIndex(e => e === max); + + const color = (max >= 0.8) ? "red" : (max >= 0.5) ? "darkorange" : "black"; + spliceAIScore = ` +
+ ${max} (${keys[index]}) +
+ `; + } + } + + const pva = ct.proteinVariantAnnotation ? ct.proteinVariantAnnotation : {}; + let uniprotAccession = "-"; + if (pva.uniprotAccession) { + uniprotAccession = ` + + ${pva.uniprotVariantId ? ` +
+ ${pva.uniprotVariantId} +
` : + ""} + `; + } + + let domains = ` + + `; + if (pva.features) { + let tooltipText = ""; + const visited = new Set(); + for (const feature of pva.features) { + if (feature.id && !visited.has(feature.id)) { + visited.add(feature.id); + tooltipText += ` +
+ ${feature.id}${feature.description} +
+ `; + } + } + domains = ` + + `; + } + + // Create the table row + const hideClass = showArrayIndexes.includes(i) ? "" : `${variantGrid._prefix}${row.id}Filtered`; + const displayStyle = showArrayIndexes.includes(i) ? "" : "display: none"; + ctHtml += ` + + + + + + + + + + + + + + + `; + } + ctHtml += "
GeneTranscriptConsequence TypeTranscript FlagsTranscript Variant AnnotationProtein Variant Annotation
cDNA / CDSCodonExon (%)SpliceAIUniProt AccPositionRef/AltDomains
${geneHtml}${transcriptIdHtml}${soArray.join("")}${transcriptFlags.join("")}${ct.cdnaPosition || "-"} / ${ct.cdsPosition || "-"}${ct.codon || "-"}${exons.join("
")}
${spliceAIScore}${uniprotAccession}${pva.position !== undefined ? pva.position : "-"}${pva.reference !== undefined ? pva.reference + "/" + pva.alternate : "-"}${domains}
"; + return ctHtml; + } + return "-"; + } + + static caddScaledFormatter(value, row, index) { + if (row && row.type !== "INDEL" && row.annotation?.functionalScore?.length > 0) { + for (const functionalScore of row.annotation.functionalScore) { + if (functionalScore.source === "cadd_scaled") { + const value = Number(functionalScore.score).toFixed(2); + if (value < 15) { + return value; + } else { + return "" + value + ""; + } + } + } + } else { + return "-"; + } + } + + static spliceAIFormatter(value, row) { + if (row.annotation.consequenceTypes?.length > 0) { + // We need to find the max Delta Score: + // Delta score of a variant, defined as the maximum of (DS_AG, DS_AL, DS_DG, DS_DL), + // ranges from 0 to 1 and can be interpreted as the probability of the variant being splice-altering. + let dscore = 0; + let transcriptId; + for (const ct of row.annotation.consequenceTypes) { + if (ct.spliceScores?.length > 0) { + const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); + if (spliceAi) { + const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); + if (max > dscore) { + dscore = max; + transcriptId = ct.transcriptId; + } + } + } + } + + const color = (dscore >= 0.8) ? "red" : (dscore >= 0.5) ? "darkorange" : "black"; + return ` +
+ ${dscore} +
+ `; + } else { + return "-"; + } + } + + static populationFrequenciesInfoTooltipContent(populationFrequencies) { + return `One coloured square is shown for each population. Frequencies are coded with colours which classify values + into 'very rare', 'rare', 'average', 'common' or 'missing', see + + https://www.nature.com/scitable/topicpage/multifactorial-inheritance-and-genetic-disease-919 + . Please, leave the cursor over each square to display the actual frequency values.
+ Note that that all frequencies are percentages. +
+
Very rare: freq < 0.1 %
+
Rare: freq < 0.5 %
+
Average: freq < 5 %
+
Common: freq >= 5 %
+
Not observed
`; + } + + // Creates the colored table with one row and as many columns as populations. + static renderPopulationFrequencies(populations, populationFrequenciesMap, populationFrequenciesColor, populationFrequenciesConfig = {displayMode: "FREQUENCY_BOX"}) { + const tooltipRows = (populations || []).map(population => { + const popFreq = populationFrequenciesMap.get(population) || null; + const altFreq = popFreq?.altAlleleFreq?.toPrecision(4) || 0; + const altCount = popFreq?.altAlleleCount || 0; + const homAltFreq = popFreq?.altHomGenotypeFreq?.toPrecision(4) || 0; + const homAltCount = popFreq?.altHomGenotypeCount || 0; + const color = VariantGridFormatter._getPopulationFrequencyColor(altFreq, populationFrequenciesColor); + let altFreqText = ""; + let homAltFreqText = ""; + + // ALT freq tell us if the VARIANT has been OBSERVED. + if (altFreq > 0) { + altFreqText = `${altFreq || "-"} / ${altCount} (${altFreq > 0 ? (altFreq * 100).toPrecision(4) + "%" : "-"})`; + homAltFreqText = `${homAltFreq > 0 ? homAltFreq : "-"} / ${homAltCount} ${homAltFreq > 0 ? `(${(homAltFreq * 100).toPrecision(4)} %)` : ""}`; + } else { + altFreqText = "Not Observed"; + homAltFreqText = "Not Observed"; + } + + return ` + + + + + + ${altFreqText} + ${homAltFreqText} + + `; + }); + const tooltip = ` + + + + + + + + + ${tooltipRows.join("")} +
PopulationAllele ALT
(freq/count)
Genotype HOM_ALT
(freq/count)
+ `; + + // Create the table (with the tooltip info) + let htmlPopFreqTable; + if (populationFrequenciesConfig?.displayMode === "FREQUENCY_BOX") { + const tableSize = populations.length * 15; + htmlPopFreqTable = ` + + + + `; + for (const population of populations) { + // This array contains "study:population" + let color = "black"; + if (typeof populationFrequenciesMap.get(population) !== "undefined") { + const freq = populationFrequenciesMap.get(population).altAlleleFreq || 0; + color = VariantGridFormatter._getPopulationFrequencyColor(freq, populationFrequenciesColor); + } + htmlPopFreqTable += ``; + } + htmlPopFreqTable += "
 
"; + } else { + htmlPopFreqTable = "
"; + const populationFrequenciesHtml = []; + for (const population of populations) { + let color = "black"; + if (typeof populationFrequenciesMap.get(population) !== "undefined") { // Freq exists + const freq = populationFrequenciesMap.get(population).altAlleleFreq || 0; + const percentage = (Number(freq) * 100).toPrecision(4); + // Only color the significant ones + if (freq <= 0.005) { + color = VariantGridFormatter._getPopulationFrequencyColor(freq, populationFrequenciesColor); + } + + if (populations.length > 1) { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`${population}`); + populationFrequenciesHtml.push(`${freq}`); + populationFrequenciesHtml.push(`(${percentage} %)`); + populationFrequenciesHtml.push("
"); + } else { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`${freq}`); + populationFrequenciesHtml.push(`(${percentage} %)`); + populationFrequenciesHtml.push("
"); + } + } else { // Freq does not exist + if (populations.length > 1) { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`${population}`); + populationFrequenciesHtml.push(`NA`); + populationFrequenciesHtml.push("
"); + } else { + populationFrequenciesHtml.push("
"); + populationFrequenciesHtml.push(`NA`); + populationFrequenciesHtml.push("
"); + } + } + } + htmlPopFreqTable += `${populationFrequenciesHtml.join("")}`; + htmlPopFreqTable += "
"; + } + + return htmlPopFreqTable; + } + + static _getPopulationFrequencyColor(freq, populationFrequenciesColor) { + let color; + if (freq === 0 || freq === "0") { + color = populationFrequenciesColor.unobserved; + } else if (freq < 0.001) { + color = populationFrequenciesColor.veryRare; + } else if (freq < 0.005) { + color = populationFrequenciesColor.rare; + } else if (freq < 0.05) { + color = populationFrequenciesColor.average; + } else { + color = populationFrequenciesColor.common; + } + return color; + } + + static clinicalTraitAssociationFormatter(value, row, index) { + const phenotypeHtml = ""; + // Check for ClinVar and Cosmic annotations + if (row?.annotation?.traitAssociation) { + // Filter the traits for this column and check the number of existing traits + const traits = row.annotation.traitAssociation.filter(trait => trait.source.name.toUpperCase() === this.field.toUpperCase()); + if (traits.length === 0) { + return ""; + } + + let tooltipText = ""; + switch (this.field) { + case "clinvar": + const results = []; + const clinicalSignificanceVisited = new Set(); + for (const trait of traits) { + let clinicalSignificance, + drugResponseClassification; + if (trait?.variantClassification?.clinicalSignificance) { + clinicalSignificance = trait.variantClassification.clinicalSignificance; + } else { + if (trait?.variantClassification?.drugResponseClassification) { + clinicalSignificance = "drug_response"; + drugResponseClassification = trait?.variantClassification?.drugResponseClassification; + } else { + clinicalSignificance = "unknown"; + } + } + let code = ""; + let color = ""; + let tooltip = ""; + switch (clinicalSignificance.toUpperCase()) { + case "BENIGN": + code = CLINICAL_SIGNIFICANCE_SETTINGS.BENIGN.id; + color = "green"; + tooltip = "Classified as benign following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "LIKELY_BENIGN": + code = CLINICAL_SIGNIFICANCE_SETTINGS.LIKELY_BENIGN.id; + color = "darkgreen"; + tooltip = "Classified as likely benign following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "VUS": + case "UNCERTAIN_SIGNIFICANCE": + code = CLINICAL_SIGNIFICANCE_SETTINGS.UNCERTAIN_SIGNIFICANCE.id; + color = "darkorange"; + tooltip = "Classified as of uncertain significance following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "LIKELY_PATHOGENIC": + code = CLINICAL_SIGNIFICANCE_SETTINGS.LIKELY_PATHOGENIC.id; + color = "darkred"; + tooltip = "Classified as likely pathogenic following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "PATHOGENIC": + code = CLINICAL_SIGNIFICANCE_SETTINGS.PATHOGENIC.id; + color = "red"; + tooltip = "Classified as pathogenic following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "DRUG_RESPONSE": + code = "DR"; + color = "darkred"; + tooltip = "Classified as drug response following ACMG/AMP recommendations for variants interpreted for Mendelian disorders"; + break; + case "UNKNOWN": + code = "NP"; + color = "grey"; + tooltip = "ClinVar submissions without an interpretation of clinical significance"; + break; + } + + if (code !== "NP" && !clinicalSignificanceVisited.has(code)) { + results.push(`${code}`); + clinicalSignificanceVisited.add(code); + } + + // Prepare the tooltip links + if (!trait.id?.startsWith("SCV")) { + // We display the link plus the clinical significance and all the heritable trait descriptions + tooltipText += ` +
+
+ ${trait.id} + + ${clinicalSignificance} ${drugResponseClassification ? "(" + drugResponseClassification + ")" : ""} + +
+
+ ${trait?.heritableTraits?.length > 0 && trait.heritableTraits + .filter(t => t.trait && t.trait !== "not specified" && t.trait !== "not provided") + .map(t => `${t.trait}`) + .join("") + } +
+
`; + } + } + + // This can only be shown if nothing else exists + if (results.length === 0) { + return "NP"; + } + + return `${results.join("
")}
`; + case "cosmic": + // Prepare the tooltip links + const cosmicMap = new Map(); + traits.forEach(trait => { + if (!cosmicMap.has(trait.id)) { + cosmicMap.set(trait.id, new Set()); + } + if (trait?.somaticInformation?.primaryHistology) { + cosmicMap.get(trait.id).add(trait.somaticInformation.primaryHistology); + } + }); + + Array.from(cosmicMap.entries()).forEach(([traitId, histologies]) => { + const histologiesItems = Array.from(histologies.values()) + .filter(histology => histology && histology !== "null") + .map(histology => `${histology}`) + .join(""); + + tooltipText += ` +
+ +
+ ${histologiesItems} +
+
+ `; + }); + + return ` + + ${cosmicMap.size} ${cosmicMap.size > 1 ? "studies" : "study" } + `; + default: + console.error("Wrong clinical source : " + this.field); + break; + } + } + return phenotypeHtml; + } + + static clinicalCancerHotspotsFormatter(value, row) { + if (row?.annotation?.cancerHotspots?.length > 0) { + const cancerHotspotsHtml = new Map(); + for (const ct of row.annotation.consequenceTypes) { + for (const hotspot of row.annotation.cancerHotspots) { + if (ct.geneName === hotspot.geneName && ct.proteinVariantAnnotation?.position === hotspot.aminoacidPosition) { + cancerHotspotsHtml.set(`${hotspot.geneName}_${hotspot.aminoacidPosition}`, hotspot); + } + } + } + let tooltipText = ""; + for (const [key, hotspot] of cancerHotspotsHtml.entries()) { + tooltipText += ` +
+
+ +
Cancer Type: ${hotspot.cancerType} - ${hotspot.variants.length} ${hotspot.variants.length === 1 ? "mutation" : "mutations"}
+
+
+ ${ + hotspot.variants + .map(variant => ` + ${AMINOACID_CODE[hotspot.aminoacidReference]}${hotspot.aminoacidPosition}${AMINOACID_CODE[variant.aminoacidAlternate]}: ${variant.count} sample(s) + `) + .join("") + } +
+
`; + } + + if (cancerHotspotsHtml.size > 0) { + return ` + + ${cancerHotspotsHtml.size} ${cancerHotspotsHtml.size === 1 ? "variant" : "variants"} + `; + } + } + return ""; + } + + static clinicalTableDetail(value, row, index) { + const clinvar = []; + const cosmic = []; + const hotspots = []; + if (row.annotation?.traitAssociation?.length > 0) { + const cosmicIntermediate = new Map(); + for (const trait of row.annotation.traitAssociation) { + const values = []; + const vcvId = trait.additionalProperties.find(p => p.name === "VCV ID"); + const genomicFeature = trait.genomicFeatures.find(f => f.featureType.toUpperCase() === "GENE"); + const reviewStatus = trait.additionalProperties.find(p => p.name === "ReviewStatus_in_source_file"); + if (trait.source.name.toUpperCase() === "CLINVAR") { + values.push(`${trait.id}`); + values.push(vcvId ? vcvId.value : trait.id); + values.push(genomicFeature?.xrefs ? genomicFeature.xrefs?.symbol : "-"); + values.push(trait.variantClassification?.clinicalSignificance); + values.push(trait.consistencyStatus); + values.push(reviewStatus ? reviewStatus.value : "-"); + values.push(trait.heritableTraits ? trait.heritableTraits.map(t => t.trait).join("
") : "-"); + clinvar.push({ + values: values + }); + } else { // COSMIC section + // Prepare data to group by histologySubtype field + const key = trait.id + ":" + trait.somaticInformation.primaryHistology + ":" + trait.somaticInformation.primaryHistology; + const reviewStatus = trait.additionalProperties.find(p => p.id === "MUTATION_SOMATIC_STATUS"); + const zygosity = trait.additionalProperties.find(p => p.id === "MUTATION_ZYGOSITY"); + if (!cosmicIntermediate.has(key)) { + cosmicIntermediate.set(key, { + id: trait.id, + url: trait.url, + primarySite: trait.somaticInformation.primarySite, + primaryHistology: trait.somaticInformation.primaryHistology, + histologySubtypes: [], + histologySubtypesCounter: new Map(), + reviewStatus: reviewStatus, + pubmed: new Set(), + zygosity: new Set() + }); + } + // Only add the new terms for this key + if (trait.somaticInformation.histologySubtype) { + if (!cosmicIntermediate.get(key).histologySubtypesCounter.get(trait.somaticInformation.histologySubtype)) { + cosmicIntermediate.get(key).histologySubtypes.push(trait.somaticInformation.histologySubtype); + } + // Increment the counter always + cosmicIntermediate.get(key).histologySubtypesCounter + .set(trait.somaticInformation.histologySubtype, cosmicIntermediate.get(key).histologySubtypesCounter.size + 1); + } + if (trait?.bibliography?.length > 0) { + cosmicIntermediate.get(key).pubmed.add(...trait.bibliography); + } + if (zygosity) { + cosmicIntermediate.get(key).zygosity.add(zygosity.value); + } + } + } + + // Sort by key and prepare column data + for (const [key, c] of new Map([...cosmicIntermediate.entries()].sort())) { + const values = []; + values.push(`${c.id}`); + values.push(c.primarySite); + values.push(c.primaryHistology); + values.push(c.histologySubtypes + .map(value => { + if (cosmicIntermediate.get(key).histologySubtypesCounter.get(value) > 1) { + return value + " (x" + cosmicIntermediate.get(key).histologySubtypesCounter.get(value) + ")"; + } else { + return "-"; + } + }) + .join("
") || "-"); + values.push(Array.from(c.zygosity?.values()).join(", ") || "-"); + values.push(c?.reviewStatus?.value || "-"); + values.push(Array.from(c.pubmed.values()).map(p => `${p}`).join("
")); + cosmic.push({ + values: values + }); + } + } + + if (row?.annotation?.cancerHotspots?.length > 0) { + const visited = {}; + for (const ct of row.annotation.consequenceTypes) { + for (const hotspot of row.annotation.cancerHotspots) { + if (ct.geneName === hotspot.geneName && ct.proteinVariantAnnotation?.position === hotspot.aminoacidPosition && !visited[hotspot.geneName + "_" + hotspot.aminoacidPosition]) { + const reference = AMINOACID_CODE[hotspot.aminoacidReference]; + const position = hotspot.aminoacidPosition; + const values = []; + values.push(hotspot.geneName); + values.push(reference); + values.push(hotspot.aminoacidPosition); + values.push(hotspot.cancerType); + values.push(hotspot.variants.length); + values.push(hotspot.variants.map(m => `${reference}${position}${AMINOACID_CODE[m.aminoacidAlternate]}: ${m.count} sample(s)`).join("; ")); + hotspots.push({ + values: values + }); + visited[hotspot.geneName + "_" + hotspot.aminoacidPosition] = true; + } + } + } + } + + // Clinvar + const clinvarColumns = [ + {title: "ID"}, + {title: "Variation ID"}, + {title: "Gene"}, + {title: "Clinical Significance"}, + {title: "Consistency Status"}, + {title: "Review Status"}, + {title: "Traits"} + ]; + const clinvarTable = VariantGridFormatter.renderTable("", clinvarColumns, clinvar, {defaultMessage: "No ClinVar data found"}); + const clinvarTraits = ` +
+ +
${clinvarTable}
+
`; + + // Cosmic + const cosmicColumns = [ + {title: "ID"}, + {title: "Primary Site"}, + {title: "Primary Histology"}, + {title: "Histology Subtype"}, + {title: "Zygosity"}, + {title: "Status"}, + {title: "Pubmed"} + ]; + const cosmicTable = VariantGridFormatter.renderTable("", cosmicColumns, cosmic, {defaultMessage: "No Cosmic data found"}); + const cosmicTraits = ` +
+ +
${cosmicTable}
+
`; + + // Cancer Hotspots + const cancerHotspotsColumns = [ + {title: "Gene Name"}, + {title: "Aminoacid Reference"}, + {title: "Aminoacid Position"}, + {title: "Cancer Type"}, + {title: "Number of Mutations"}, + {title: "Mutations"}, + ]; + const cancerHotspotsTable = VariantGridFormatter.renderTable("", cancerHotspotsColumns, hotspots, {defaultMessage: "No Cancer Hotspots data found"}); + const cancerHotspotsHtml = ` +
+ +
${cancerHotspotsTable}
+
`; + + return clinvarTraits + cosmicTraits + cancerHotspotsHtml; + } + + /* + * Reported Variant formatters + */ + static toggleDetailClinicalEvidence(e) { + const id = e.target.dataset.id; + const elements = document.getElementsByClassName(this._prefix + id + "EvidenceFiltered"); + for (const element of elements) { + if (element.style.display === "none") { + element.style.display = ""; + } else { + element.style.display = "none"; + } + } + } + + static reportedVariantFormatter(value, variant, index) { + return ` + ${variant?.interpretations?.length > 0 ? ` +
${variant.interpretations.length === 1 ? "1 case found" : `${variant.interpretations.length} cases found`}
+
+
REPORTED: ${variant.interpretationStats?.status?.REPORTED || 0} times
+
TIER 1: ${variant.interpretationStats?.tier?.TIER1 || 0} times
+
DISCARDED: ${variant.interpretationStats?.status?.DISCARDED || 0} times
+
` : ` +
No cases found
` + } + `; + } + + static reportedVariantDetailFormatter(value, row, opencgaSession) { + if (row?.interpretations?.length > 0) { + let reportedHtml = ` + + + + + + + + + + + + + + + + + + + + `; + + for (const interpretation of row.interpretations) { + // Prepare data info for columns + const caseId = interpretation.id.split(".")[0]; + const interpretationIdHtml = ` +
+ +
+ `; + + const panelsHtml = ` +
+ ${interpretation.panels?.map(panel => { + if (panel?.source?.project === "PanelApp") { + return `${panel.name}`; + } else { + return `${panel.name || "-"}`; + } + })?.join("
")} +
`; + + const interpretedVariant = interpretation.primaryFindings.find(variant => variant.id === row.id); + + const sampleHtml = ` +
+ +
`; + + const genotype = VariantInterpreterGridFormatter.alleleGenotypeRenderer(row, interpretedVariant.studies[0]?.samples[0], "call"); + const genotypeHtml = ` +
+ ${genotype || "-"} +
`; + + const statusHtml = ` +
+ ${interpretedVariant.status || "-"} +
`; + const discussionHtml = ` +
+ ${interpretedVariant?.discussion?.text || "-"} +
`; + + const genes = []; + const transcripts = []; + const acmgClassifications = []; + const tierClassifications = []; + const clinicalSignificances = []; + for (const evidence of interpretedVariant.evidences.filter(ev => ev.review.select)) { + genes.push(` + + ${evidence.genomicFeature.geneName} + + `); + transcripts.push(` + + ${evidence.genomicFeature.transcriptId} + + `); + acmgClassifications.push(evidence.review.acmg?.map(acmg => acmg.classification)?.join(", ")|| "-"); + tierClassifications.push(evidence.review.tier || "-"); + clinicalSignificances.push(` + + ${evidence.review.clinicalSignificance || "-"} + + `); + } + + // Create the table row + reportedHtml += ` + + + + + + + + + + + + + + + `; + } + reportedHtml += "
CaseDisease PanelSampleGenotypeStatusDiscussionEvidences
GeneTranscriptACMGTierClinical Significance
${interpretationIdHtml}${interpretation?.panels?.length > 0 ? panelsHtml : "-"}${sampleHtml}${genotypeHtml}${statusHtml}${discussionHtml}${genes.length > 0 ? genes.join("
") : "-"}
${transcripts.length > 0 ? transcripts.join("
") : "-"}
${acmgClassifications.length > 0 ? acmgClassifications.join("
") : "-"}
${tierClassifications.length > 0 ? tierClassifications.join("
") : "-"}
${clinicalSignificances.length > 0 ? clinicalSignificances.join("
") : "-"}
"; + return reportedHtml; + } + return "-"; + } + +} diff --git a/src/webcomponents/variant/variant-grid-formatter.js b/src/webcomponents/variant/variant-grid-formatter.js index 177c4c0620..763af39f5c 100644 --- a/src/webcomponents/variant/variant-grid-formatter.js +++ b/src/webcomponents/variant/variant-grid-formatter.js @@ -14,6 +14,7 @@ * limitations under the License. */ +import VariantFormatter from "./variant-formatter.js"; import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; import VariantInterpreterGridFormatter from "./interpretation/variant-interpreter-grid-formatter"; import CustomActions from "../commons/custom-actions.js"; @@ -62,28 +63,29 @@ export default class VariantGridFormatter { return result; } - static variantFormatter(value, row, index, assembly, config = {}) { - if (!row) { + static variantIdFormatter(id, variant, index, assembly, config = {}) { + if (!variant) { return; } - let ref = row.reference ? row.reference : "-"; - let alt = row.alternate ? row.alternate : "-"; - - // Check size - const maxAlleleLength = config?.alleleStringLengthMax ? config.alleleStringLengthMax : 20; - ref = (ref.length > maxAlleleLength) ? ref.substring(0, 4) + "..." + ref.substring(ref.length - 4) : ref; - alt = (alt.length > maxAlleleLength) ? alt.substring(0, 4) + "..." + alt.substring(alt.length - 4) : alt; - - // Ww need to escape < and > symbols from , , ... - alt = alt.replaceAll("<", "<").replaceAll(">", ">"); + // let ref = variant.reference ? variant.reference : "-"; + // let alt = variant.alternate ? variant.alternate : "-"; + // + // // Check size + // const maxAlleleLength = config?.alleleStringLengthMax ? config.alleleStringLengthMax : 20; + // ref = (ref.length > maxAlleleLength) ? ref.substring(0, 4) + "..." + ref.substring(ref.length - 4) : ref; + // alt = (alt.length > maxAlleleLength) ? alt.substring(0, 4) + "..." + alt.substring(alt.length - 4) : alt; + // + // // Ww need to escape < and > symbols from , , ... + // alt = alt.replaceAll("<", "<").replaceAll(">", ">"); + const variantId = VariantFormatter.variantIdFormatter(id, variant); // Create links for tooltip let tooltipText = ""; - const variantRegion = row.chromosome + ":" + row.start + "-" + row.end; + const variantRegion = variant.chromosome + ":" + variant.start + "-" + variant.end; // 1. Add Decipher only if variant is a SNV or we have the original call. INDELS cannot be linked in the Variant Browser - if (row.id || row.studies[0]?.files[0]?.call?.variantId) { - const variantId = row.studies[0]?.files[0]?.call?.variantId?.split(",")[0] || row.id; + if (variant.id || variant.studies[0]?.files[0]?.call?.variantId) { + const variantId = variant.studies[0]?.files[0]?.call?.variantId?.split(",")[0] || variant.id; tooltipText += `
@@ -92,8 +94,8 @@ export default class VariantGridFormatter {
`; @@ -102,18 +104,27 @@ export default class VariantGridFormatter { tooltipText += ` `; - const snpHtml = VariantGridFormatter.snpFormatter(value, row, index, assembly); + // const snpHtml = VariantGridFormatter.snpFormatter(value, row, index, assembly); + const snpId = VariantFormatter.snpFormatter(id, variant, index, assembly); + let snpHtml; + if (snpId) { + if (assembly.toUpperCase() === "GRCH37") { + snpHtml = "" + snpId + ""; + } else { + snpHtml = "" + snpId + ""; + } + } // Add highlight icons let iconHighlights = []; @@ -121,7 +132,7 @@ export default class VariantGridFormatter { iconHighlights = config.highlights .filter(h => h.active) .map(highlight => { - if (CustomActions.get(highlight).execute(row, highlight) && highlight.style?.icon) { + if (CustomActions.get(highlight).execute(variant, highlight) && highlight.style?.icon) { const description = highlight.description || highlight.name || ""; const icon = highlight.style.icon; const color = highlight.style.iconColor || ""; @@ -134,7 +145,7 @@ export default class VariantGridFormatter { return ` @@ -142,44 +153,44 @@ export default class VariantGridFormatter { `; } - static snpFormatter(value, row, index, assembly) { - // We try first to read SNP ID from the 'names' of the variant (this identifier comes from the file). - // If this ID is not a "rs..." then we search the rs in the CellBase XRef annotations. - // This field is in annotation.xref when source: "dbSNP". - let snpId = ""; - if (row.names && row.names.length > 0) { - for (const name of row.names) { - if (name.startsWith("rs")) { - snpId = name; - break; - } - } - } else { - if (row.annotation) { - if (row.annotation.id && row.annotation.id.startsWith("rs")) { - snpId = row.annotation.id; - } else { - if (row.annotation.xrefs) { - for (const xref of row.annotation.xrefs) { - if (xref.source === "dbSNP") { - snpId = xref.id; - break; - } - } - } - } - } - } - - if (snpId) { - if (assembly.toUpperCase() === "GRCH37") { - return "" + snpId + ""; - } else { - return "" + snpId + ""; - } - } - return snpId; - } + // static snpFormatter(value, row, index, assembly) { + // // We try first to read SNP ID from the 'names' of the variant (this identifier comes from the file). + // // If this ID is not a "rs..." then we search the rs in the CellBase XRef annotations. + // // This field is in annotation.xref when source: "dbSNP". + // let snpId = ""; + // if (row.names && row.names.length > 0) { + // for (const name of row.names) { + // if (name.startsWith("rs")) { + // snpId = name; + // break; + // } + // } + // } else { + // if (row.annotation) { + // if (row.annotation.id && row.annotation.id.startsWith("rs")) { + // snpId = row.annotation.id; + // } else { + // if (row.annotation.xrefs) { + // for (const xref of row.annotation.xrefs) { + // if (xref.source === "dbSNP") { + // snpId = xref.id; + // break; + // } + // } + // } + // } + // } + // } + // + // if (snpId) { + // if (assembly.toUpperCase() === "GRCH37") { + // return "" + snpId + ""; + // } else { + // return "" + snpId + ""; + // } + // } + // return snpId; + // } static geneFormatter(variant, index, query, opencgaSession, gridCtSettings) { // FIXME diff --git a/src/webcomponents/variant/variant-table-formatter.js b/src/webcomponents/variant/variant-table-formatter.js index f1a2dbd9fb..b734c7ea94 100644 --- a/src/webcomponents/variant/variant-table-formatter.js +++ b/src/webcomponents/variant/variant-table-formatter.js @@ -14,9 +14,9 @@ * limitations under the License. */ +import VariantFormatter from "./variant-formatter"; import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; -import VariantInterpreterGridFormatter from "./interpretation/variant-interpreter-grid-formatter"; -import CustomActions from "../commons/custom-actions.js"; +import VariantInterpreterGridFormatter from "./interpretation/variant-interpreter-grid-formatter.js"; export default class VariantTableFormatter { @@ -62,29 +62,15 @@ export default class VariantTableFormatter { return result; } - static variantIdFormatter(id, variant) { - let ref = variant.reference ? variant.reference : "-"; - let alt = variant.alternate ? variant.alternate : "-"; - - // Check size - const maxAlleleLength = 20; - ref = (ref.length > maxAlleleLength) ? ref.substring(0, 4) + "..." + ref.substring(ref.length - 4) : ref; - alt = (alt.length > maxAlleleLength) ? alt.substring(0, 4) + "..." + alt.substring(alt.length - 4) : alt; - - // Ww need to escape < and > symbols from , , ... - alt = alt.replaceAll("<", "<").replaceAll(">", ">"); - return `${variant.chromosome}:${variant.start} ${ref}/${alt}`; - } - - static variantFormatter() { + static variantIdFormatter() { return { title: "Variant ID", field: "id", type: "basic", display: { format: (id, variant) => { - const variantId = VariantTableFormatter.variantIdFormatter(id, variant); - const snpId = VariantTableFormatter.snpFormatter(id, variant); + const variantId = VariantFormatter.variantIdFormatter(id, variant); + const snpId = VariantFormatter.snpFormatter(id, variant); if (snpId) { return `${variantId} (${snpId}`; } else { @@ -98,137 +84,6 @@ export default class VariantTableFormatter { }; } - static snpFormatter(value, row) { - // We try first to read SNP ID from the 'names' of the variant (this identifier comes from the file). - // If this ID is not a "rs..." then we search the rs in the CellBase XRef annotations. - // This field is in annotation.xref when source: "dbSNP". - let snpId = ""; - if (row.names && row.names.length > 0) { - for (const name of row.names) { - if (name.startsWith("rs")) { - snpId = name; - break; - } - } - } else { - if (row.annotation) { - if (row.annotation.id && row.annotation.id.startsWith("rs")) { - snpId = row.annotation.id; - } else { - if (row.annotation.xrefs) { - for (const xref of row.annotation.xrefs) { - if (xref.source === "dbSNP") { - snpId = xref.id; - break; - } - } - } - } - } - } - - return snpId; - } - - static geneIdFormatter(variant) { - if (!variant.annotation) { - variant.annotation = { - consequenceTypes: [] - }; - } - // const {selectedConsequenceTypes, notSelectedConsequenceTypes} = - // VariantTableFormatter._consequenceTypeDetailFormatterFilter(variant.annotation.consequenceTypes, gridCtSettings); - - // Keep a map of genes and the SO accessions and names - // const geneHasQueryCt = new Set(); - // if (query?.ct) { - // const consequenceTypes = new Set(); - // for (const ct of query.ct.split(",")) { - // consequenceTypes.add(ct); - // } - // - // for (const ct of selectedConsequenceTypes) { - // if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { - // geneHasQueryCt.add(ct.geneName); - // } - // } - // } - - if (variant?.annotation?.consequenceTypes?.length > 0) { - const visited = {}; - const genes = []; - // const geneWithCtLinks = []; - for (let i = 0; i < variant.annotation.consequenceTypes.length; i++) { - const geneName = variant.annotation.consequenceTypes[i].geneName; - - // We process Genes just one time - if (geneName && !visited[geneName]) { - // let geneViewMenuLink = ""; - // if (opencgaSession.project && opencgaSession.study) { - // geneViewMenuLink = `
- // Gene View - //
`; - // } - - // const tooltipText = ` - // ${geneViewMenuLink} - // ${this.getGeneTooltip(geneName, this.opencgaSession?.project?.organism?.assembly)} - // `; - - // If query.ct exists - // if (query?.ct) { - // // If gene contains one of the query.ct - // if (geneHasQueryCt.has(geneName)) { - // geneWithCtLinks.push(` - // ${geneName} - // `); - // } else { - // geneLinks.push(` - // ${geneName} - // `); - // } - // } else { - // // No query.ct passed - // geneLinks.push(` - // ${geneName} - // `); - // } - - genes.push(geneName); - visited[geneName] = true; - } - } - - // Do not write more than 4 genes per line, this could be easily configurable - // let resultHtml = ""; - // const maxDisplayedGenes = 10; - // const allGenes = geneWithCtLinks.concat(geneLinks); - - // if (allGenes.length <= maxDisplayedGenes) { - // resultHtml = allGenes.join(","); - // } else { - // resultHtml = ` - //
- // ${allGenes.slice(0, maxDisplayedGenes).join(",")} - // - // ,${allGenes.slice(maxDisplayedGenes).join(",")} - // - // - //
- // `; - // } - return genes.join(", "); - } else { - return "-"; - } - } static geneFormatter() { return { @@ -237,7 +92,23 @@ export default class VariantTableFormatter { type: "basic", display: { format: (id, variant) => { - return VariantTableFormatter.geneIdFormatter(variant); + if (variant?.annotation?.consequenceTypes?.length > 0) { + const visited = {}; + const genes = []; + // const geneWithCtLinks = []; + for (let i = 0; i < variant.annotation.consequenceTypes.length; i++) { + const geneName = variant.annotation.consequenceTypes[i].geneName; + + // We process Genes just one time + if (geneName && !visited[geneName]) { + genes.push(geneName); + visited[geneName] = true; + } + } + return genes.join(", "); + } else { + return "-"; + } }, // style: { // "font-weight": "bold", @@ -246,34 +117,6 @@ export default class VariantTableFormatter { }; } - static getGeneTooltip(geneName, assembly) { - return ` - -
- Ensembl -
-
- LRG -
-
- UniProt -
-
- Varsome -
- - -
- Decipher -
-
- COSMIC -
-
- OMIM -
- `; - } static hgvsFormatter(variant, gridConfig) { BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); From b23e886d9185b961cdf5afc449c7139b586fe6b4 Mon Sep 17 00:00:00 2001 From: imedina Date: Tue, 21 Nov 2023 01:22:26 +0000 Subject: [PATCH 028/153] wc: several improvements and fixes in data-form. New fucntionality added such as 'links' in complex elements --- .../variant-browser-grid-test.js | 81 +- src/webcomponents/commons/forms/data-form.js | 285 ++-- .../variant/variant-formatter.js | 1198 ++--------------- .../variant/variant-table-formatter.js | 747 +++++----- 4 files changed, 695 insertions(+), 1616 deletions(-) diff --git a/src/sites/test-app/webcomponents/variant-browser-grid-test.js b/src/sites/test-app/webcomponents/variant-browser-grid-test.js index 91675f9172..c5ac78a7e2 100644 --- a/src/sites/test-app/webcomponents/variant-browser-grid-test.js +++ b/src/sites/test-app/webcomponents/variant-browser-grid-test.js @@ -152,6 +152,31 @@ class VariantBrowserGridTest extends LitElement { } getDefaultConfig() { + const gridConfig = { + ...(this.opencgaSession?.user?.configs?.IVA?.settings?.[this.gridTypes.rearrangements]?.grid || {}), + somatic: false, + geneSet: { + ensembl: true, + refseq: false, + }, + consequenceType: { + maneTranscript: true, + gencodeBasicTranscript: true, + ensemblCanonicalTranscript: true, + refseqTranscript: true, + ccdsTranscript: false, + ensemblTslTranscript: false, + proteinCodingTranscript: false, + highImpactConsequenceTypeTranscript: false, + + showNegativeConsequenceTypes: true + }, + populationFrequenciesConfig: { + displayMode: "FREQUENCY_BOX" + }, + variantTypes: ["SNV"], + }; + return { title: "Summary", icon: "", @@ -174,6 +199,7 @@ class VariantBrowserGridTest extends LitElement { field: "variants", type: "table", display: { + defaultLayout: "vertical", className: "", style: "", headerClassName: "", @@ -187,25 +213,42 @@ class VariantBrowserGridTest extends LitElement { defaultValue: "-", columns: [ VariantTableFormatter.variantIdFormatter(), - VariantTableFormatter.geneFormatter() - // { - // title: "Somatic", - // type: "custom", - // field: "somatic", - // // formatter: value => value ? "true" : "false", - // display: { - // render: somatic => somatic ? "true" : "false", - // style: { - // color: "red" - // } - // } - // }, - // { - // title: "Phenotypes", - // field: "phenotypes", - // type: "list" - // // formatter: value => value?.length ? `${value.map(d => d.id).join(", ")}` : "-", - // }, + VariantTableFormatter.geneFormatter(), + VariantTableFormatter.hgvsFormatter(gridConfig), + VariantTableFormatter.typeFormatter(), + VariantTableFormatter.consequenceTypeFormatter("", gridConfig), + { + title: "Deleteriousness", + display: { + columns: [ + VariantTableFormatter.siftFormatter(), + VariantTableFormatter.polyphenFormatter(), + VariantTableFormatter.revelFormatter(), + VariantTableFormatter.caddScaledFormatter(), + VariantTableFormatter.spliceAIFormatter(), + ] + } + }, + { + title: "Conservation", + display: { + columns: [ + VariantTableFormatter.conservationFormatter("PhyloP"), + VariantTableFormatter.conservationFormatter("PhastCons"), + VariantTableFormatter.conservationFormatter("GERP"), + ] + } + }, + // VariantTableFormatter.populationFrequencyFormatter(), + { + title: "Clinical", + display: { + columns: [ + VariantTableFormatter.clinvarFormatter(), + VariantTableFormatter.cosmicFormatter(), + ] + } + } ], }, }, diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index b006beb4b9..95d6abc2d3 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -103,71 +103,66 @@ export default class DataForm extends LitElement { this.data = this.data ?? {}; } - getValue(field, object, defaultValue, display) { - let value = object; + getValue(field, object = this.data, defaultValue, display) { + let value; if (field) { - const _object = object ? object : this.data; // If field contains [] means the element type is object-list, // we need to get the value from the array, information is encoded as: - // phenotypes[].1.id: field id from second item of phenotypes + // phenotypes[].1.id: field id from second item of phenotypes if (field.includes("[]")) { const [parentItemArray, right] = field.split("[]."); if (right?.includes(".")) { const [itemIndex, ...itemFieldIds] = right.split("."); - // support object nested + // Support nested object if (itemFieldIds.length === 1) { - value = UtilsNew.getObjectValue(_object, parentItemArray, "")[itemIndex][itemFieldIds[0]]; + value = UtilsNew.getObjectValue(object, parentItemArray, "")[itemIndex][itemFieldIds[0]]; } else { - value = UtilsNew.getObjectValue(_object, parentItemArray, "")[itemIndex][itemFieldIds[0]]?.[itemFieldIds[1]]; + value = UtilsNew.getObjectValue(object, parentItemArray, "")[itemIndex][itemFieldIds[0]]?.[itemFieldIds[1]]; } } else { // FIXME this should never be reached - console.error("this should never be reached"); - // value = this.objectListItems[parentItemArray]?.[right]; + console.error("This should never be reached"); } } else { // Optional chaining is needed when "res" is undefined - value = field.split(".").reduce((res, prop) => res?.[prop], _object); + value = field.split(".").reduce((res, prop) => res?.[prop], object); } - // Check if 'format' exists - if (value && display?.format) { - // check if response is actually an HTML - // value = UtilsNew.renderHTML(display.format(value)); - value = display.format(value, object); - } - - // Needed for handling falsy values - if (value !== undefined && value !== "") { - if (display) { - if (display.className || display.classes || display.style) { - const style = this._parseStyleField(display.style); - value = html`${value}`; - } - if (display.link) { - value = html`${value}`; - } - if (display.decimals && !isNaN(value)) { - value = value.toFixed(display.decimals); - } + // If 'value' exists we must apply the functions, DO NOT change the order + if (value) { + if (display?.format) { + // Check if response is actually an HTML + // value = UtilsNew.renderHTML(display.format(value)); + value = display.format(value, object); + } + if (display?.link) { + const href = display.link.replace(field.toUpperCase(), value); + value = html`${value}`; + } + if (display?.className || display?.classes || display?.style) { + const style = this._parseStyleField(display.style, value, object); + value = html`${value}`; + } + // TODO this should be deprecated, we can use 'format' now + if (display?.decimals && !isNaN(value)) { + value = value.toFixed(display.decimals); } } else { value = defaultValue; } - } else if (defaultValue) { + } else { value = defaultValue; } return value; } - applyTemplate(template, object, defaultValue, element) { + applyTemplate(template, data, defaultValue, element) { // Parse template string and find matches groups const matches = template - .match(/\$\{[a-zA-Z_.\[\]]+\}/g) + .match(/\$\{[a-zA-Z_.\[\]]+}/g) .map(elem => elem.substring(2, elem.length - 1)); for (const match of matches) { - let value = this.getValue(match, object, defaultValue); // Check if 'style' has been defined for this match variable, example: // { // title: "Format", @@ -185,14 +180,22 @@ export default class DataForm extends LitElement { // bioformat: { // "color": "green" // } + // }, + // link: { + // format: (value, data) => https://... // } // }, // }, + let value = this.getValue(match, data, defaultValue); if (element?.display?.format?.[match]) { - value = element?.display?.format?.[match](value, object); + value = element?.display?.format?.[match](value, data); + } + if (element?.display?.link?.[match]) { + const href = element?.display?.link?.[match](value, data); + value = `${value}`; } if (element?.display?.style?.[match]) { - const style = this._parseStyleField(element?.display?.style?.[match]); + const style = this._parseStyleField(element.display.style[match], value, data); value = `${value}`; } // eslint-disable-next-line no-param-reassign @@ -203,12 +206,21 @@ export default class DataForm extends LitElement { } // Convert a 'string' or 'object' field to the HTML style string, ie. "font-weight: bold;color:red" - _parseStyleField(elementStyle) { + _parseStyleField(elementStyle, value, data = this.data) { let style = elementStyle || ""; if (elementStyle && typeof elementStyle === "object") { - style = Object.entries(elementStyle) - .map(([k, v]) => `${k}: ${v}`) - .join(";"); + const styles = []; + for (const [k, v] of Object.entries(elementStyle)) { + if (typeof v === "string") { + styles.push(`${k}: ${v}`); + } else { + if (typeof v === "function" && value) { + const value2 = v(value, data); + styles.push(`${k}: ${value2}`); + } + } + } + style = styles.join(";"); } return style; } @@ -619,7 +631,7 @@ export default class DataForm extends LitElement { content = this._createListElement(element, this.data, section); break; case "table": - content = this._createTableElement(element, section); + content = this._createTableElement(element, this.data, section); break; case "image": content = this._createImageElement(element); @@ -629,7 +641,7 @@ export default class DataForm extends LitElement { content = this._createPlotElement(element); break; case "json": - content = this._createJsonElement(element); + content = this._createJsonElement(element, section); break; case "tree": content = this._createTreeElement(element); @@ -1081,13 +1093,18 @@ export default class DataForm extends LitElement { _createListElement(element, data = this.data, section) { // Get values - let array = this.getValue(element.field, data); + let array; + if (element.field) { + array = this.getValue(element.field, data); + } else { + array = element.display.getData(data); + } const contentLayout = element.display?.contentLayout || "vertical"; // 1. Check array and layout exist if (!Array.isArray(array)) { return this._createElementTemplate(element, null, null, { - message: `Field '${element.field}' is not an array`, + message: this._getDefaultValue(element, section) ?? `Field '${element.field}' is not an array`, className: "text-danger" }); } @@ -1110,11 +1127,11 @@ export default class DataForm extends LitElement { if (array.length === 0) { // If empty we just print the defaultValue, this is not an error return this._createElementTemplate(element, null, null, { - message: this._getDefaultValue(element, section), + message: this._getDefaultValue(element, section) ?? "Empty array", }); } - // 4. Initialise values with array, this is valid for scalars, or when 'template' and 'format' do not exist + // 4. Format list elements. Initialise values with array, this is valid for scalars, or when 'template' and 'format' do not exist // Apply the template to all Array elements and store them in 'values' let values = array; if (element.display?.format || element.display?.render) { @@ -1131,28 +1148,72 @@ export default class DataForm extends LitElement { } } - // 5. Render element values + // 5. Precompute styles + const styles = {}; + if (element.display?.style) { + if (typeof element.display.style === "string") { + // All elements will have the same style + array.forEach(item => styles[item] = element.display.style); + } else { + // It is an object, we must find the right style for each element + for (const item of array) { + // This call already checks if style is a function + styles[item] = this._parseStyleField(element.display?.style, item, data); + } + } + } + + // 6. Precompute separators + const separators = {}; + if (element.display?.separator) { + // Last element cannot add a separator, so we iterate until length -1 + for (let i = 0; i < array.length - 1; i++) { + let separator = null; + if (typeof element.display.separator === "string") { + separator = element.display.separator; + } else { + separator = element.display.separator(array[i], i, array, data); + } + if (separator) { + separators[array[i]] = separator.includes("---") ? "
" : separator; + } + } + } + + // 7. Render element values let content = this._getDefaultValue(element, section); + // const separator = element?.display?.separator; switch (contentLayout) { case "horizontal": - const separator = element?.display?.separator ?? ", "; content = ` ${values - .map(elem => `${elem}`) - .join(`${separator}`) + .map((elem, index) => ` + ${elem} + ${index < array.length - 1 ? separators[elem] ?? ", " : ""} + `) + .join(``) }`; break; case "vertical": content = ` ${values - .map(elem => `
${elem}
`) + .map(elem => ` +
${elem}
+ ${separators[elem] ? `
${separators[elem]}
` : ""} + `) .join("") }`; break; case "bullets": content = `
    - ${values.map(elem => `
  • ${elem}
  • `).join("")} + ${values + .map(elem => ` +
  • ${elem}
  • + ${separators[elem] ? `
    ${separators[elem]}
    ` : ""} + `) + .join("") + }
`; break; @@ -1162,9 +1223,15 @@ export default class DataForm extends LitElement { } - _createTableElement(element, section) { - // Get valid parameters - let array = this.getValue(element.field); + _createTableElement(element, data = this.data, section) { + // Get array values + let array; + if (element.field) { + array = this.getValue(element.field, data); + } else { + array = element.display.getData(data); + } + const tableClassName = element.display?.className || ""; const tableStyle = this._parseStyleField(element.display?.style) || ""; const headerClassName = element.display?.headerClassName || ""; @@ -1212,50 +1279,77 @@ export default class DataForm extends LitElement { }); } + // 4. Check for double columns + const supraColumns = []; + const subColumns = []; + const columns = []; + for (const column of element.display.columns) { + // Add first level columns as supra columns + supraColumns.push(column); + if (column.display?.columns) { + // Add nested columns as sub columns for the table header + subColumns.push(...column.display.columns); + // Add nested columns as real data columns + columns.push(...column.display.columns); + } else { + // When no sub columns are found then just add the data column + columns.push(column); + } + } + + // 5. Render the table const content = html` ${headerVisible ? html` - - ${element.display.columns.map(elem => html` - - `)} - + ${supraColumns.length > 0 ? html` + + ${supraColumns.map(elem => html` + + `)} + + ` : nothing} + ${subColumns.length > 0 ? html` + + ${subColumns.map(elem => html` + ` + )} + + ` : nothing} ` : nothing} ${array .map(row => html` - ${element.display.columns - .map(elem => { - const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; - const elemStyle = this._parseStyleField(elem.display?.style); - - // Check the element type - let content; - switch (elem.type) { - case "complex": - content = this._createComplexElement(elem, row); - break; - case "list": - content = this._createListElement(elem, row, section); - break; - case "image": - content = this._createImageElement(elem); - break; - case "custom": - content = elem.display?.render(this.getValue(elem.field, row)); - break; - default: - content = this.getValue(elem.field, row, this._getDefaultValue(element, section), elem.display); - } + ${columns.map(elem => { + const elemClassName = elem.display?.className ?? elem.display?.classes ?? ""; + const elemStyle = this._parseStyleField(elem.display?.style); + + // Check the element type + let content; + switch (elem.type) { + case "complex": + content = this._createComplexElement(elem, row); + break; + case "list": + content = this._createListElement(elem, row, section); + break; + case "image": + content = this._createImageElement(elem); + break; + case "custom": + content = elem.display?.render(this.getValue(elem.field, row)); + break; + default: + content = this.getValue(elem.field, row, this._getDefaultValue(element, section), elem.display); + } - return html` - - `; - })} + return html` + + `; + })} `)} @@ -1280,7 +1374,7 @@ export default class DataForm extends LitElement { } _createImageElement(element) { - const value = (element.display?.image) ? element.display.image(this.data) : this.getValue(element.field); + const value = (element.field) ? this.getValue(element.field) : element.display?.getData(this.data); const content = html` @@ -1341,8 +1435,8 @@ export default class DataForm extends LitElement { } } - _createJsonElement(element) { - const json = this.getValue(element.field, this.data, this._getDefaultValue(element)); + _createJsonElement(element, section) { + const json = this.getValue(element.field, this.data, this._getDefaultValue(element, section)); let content = ""; (json.length || UtilsNew.isObject(json)) ? content = html` @@ -1553,12 +1647,13 @@ export default class DataForm extends LitElement { // We create 'virtual' element fields: phenotypes[].1.id, by doing this all existing // items have a virtual element associated, this will allow to get the proper value later. for (let i = 0; i< _element.elements.length; i++) { - // This support object nested + // This support nested object const [left, right] = _element.elements[i].field.split("[]."); _element.elements[i].field = left + "[]." + index + "." + right; if (_element.elements[i].type === "custom") { _element.elements[i].display.render = element.elements[i].display.render; } + // Copy JSON stringify and parse ignores functions, we need to copy them if (_element.elements[i].type === "select" && typeof element.elements[i].allowedValues === "function") { _element.elements[i].allowedValues = element.elements[i].allowedValues; } @@ -1568,10 +1663,6 @@ export default class DataForm extends LitElement { if (typeof element.elements[i]?.save === "function") { _element.elements[i].save = element.elements[i].save; } - // if (typeof element.elements[i]?.validation?.message === "function") { - // _element.elements[i].validation.message = element.elements[i].validation.message; - // } - // Copy JSON stringify and parse ignores functions, we need to copy them if (typeof element.elements[i]?.display?.disabled === "function") { _element.elements[i].display.disabled = element.elements[i].display.disabled; } diff --git a/src/webcomponents/variant/variant-formatter.js b/src/webcomponents/variant/variant-formatter.js index 08b33d5da2..e8e331d2d5 100644 --- a/src/webcomponents/variant/variant-formatter.js +++ b/src/webcomponents/variant/variant-formatter.js @@ -66,546 +66,102 @@ export default class VariantFormatter { return snpId; } - static hgvsFormatter(variant, gridConfig) { - BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); - const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(variant.annotation?.consequenceTypes, gridConfig).indexes; - - if (showArrayIndexes?.length > 0 && variant.annotation.hgvs?.length > 0) { - const results = []; - for (const index of showArrayIndexes) { - const consequenceType = variant.annotation.consequenceTypes[index]; - const hgvsTranscriptIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.transcriptId)); - const hgvsProteingIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.proteinVariantAnnotation?.proteinId)); - if (hgvsTranscriptIndex > -1 || hgvsProteingIndex > -1) { - results.push(` -
- ${VariantGridFormatter.getHgvsLink(consequenceType.transcriptId, variant.annotation.hgvs) || "-"} -
-
- ${VariantGridFormatter.getHgvsLink(consequenceType.proteinVariantAnnotation?.proteinId, variant.annotation.hgvs) || "-"} -
- `); - } - } - return results.join("
"); - } - } - - static vcfFormatter(value, row, field, type = "INFO") { - if (type.toUpperCase() === "INFO") { - return row.studies[0].files[0].data[field]; - } else { - const index = row.studies[0].sampleDataKeys.findIndex(f => f === field); - return row.studies[0].samples[0].data[index]; - } - } - - static typeFormatter(value, row) { - if (row) { - let type = row.type; - let color = ""; - switch (row.type) { - case "SNP": // Deprecated - type = "SNV"; - color = "black"; - break; - case "INDEL": - case "CNV": // Deprecated - case "COPY_NUMBER": - case "COPY_NUMBER_GAIN": - case "COPY_NUMBER_LOSS": - case "MNV": - color = "darkorange"; - break; - case "SV": - case "INSERTION": - case "DELETION": - case "DUPLICATION": - case "TANDEM_DUPLICATION": - case "BREAKEND": - color = "red"; - break; - default: - color = "black"; - break; - } - return `${type}`; - } else { - return "-"; - } - } - - static consequenceTypeFormatter(value, row, ctQuery, gridCtSettings) { - if (row?.annotation && row.annotation.consequenceTypes?.length > 0) { - let {selectedConsequenceTypes, notSelectedConsequenceTypes, indexes} = - VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, gridCtSettings); - - // If CT is passed in the query then we must make and AND with the selected transcript by the user. - // This means that only the selectedConsequenceTypes that ARE ALSO IN THE CT QUERY are displayed. - if (ctQuery) { - const consequenceTypes = new Set(); - for (const ct of ctQuery.split(",")) { - consequenceTypes.add(ct); - } - - const newSelectedConsequenceTypes = []; - for (const ct of selectedConsequenceTypes) { - if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { - newSelectedConsequenceTypes.push(ct); - } else { - notSelectedConsequenceTypes.push(ct); - } - } - selectedConsequenceTypes = newSelectedConsequenceTypes; - } - - const positiveConsequenceTypes = []; - const negativeConsequenceTypes = []; - const soVisited = new Set(); - for (const ct of selectedConsequenceTypes) { - for (const so of ct.sequenceOntologyTerms) { - if (!soVisited.has(so?.name)) { - positiveConsequenceTypes.push(`${so.name}`); - soVisited.add(so.name); - } - } - } - - // Print negative SO, if not printed as positive - let negativeConsequenceTypesText = ""; - if (gridCtSettings.consequenceType.showNegativeConsequenceTypes) { - for (const ct of notSelectedConsequenceTypes) { - for (const so of ct.sequenceOntologyTerms) { - if (!soVisited.has(so.name)) { - negativeConsequenceTypes.push(`
${so.name}
`); - soVisited.add(so.name); + static siftPproteinScoreFormatter(value, variant) { + let min = 10; + let description = ""; + if (variant?.annotation?.consequenceTypes?.length > 0) { + for (let i = 0; i < variant.annotation.consequenceTypes.length; i++) { + if (variant.annotation.consequenceTypes[i]?.proteinVariantAnnotation?.substitutionScores) { + for (let j = 0; j < variant.annotation.consequenceTypes[i].proteinVariantAnnotation.substitutionScores.length; j++) { + const substitutionScore = variant.annotation.consequenceTypes[i].proteinVariantAnnotation.substitutionScores[j]; + if (substitutionScore.source === "sift" && substitutionScore.score < min) { + min = substitutionScore.score; + description = substitutionScore.description; } } } - - if (negativeConsequenceTypes.length > 0) { - negativeConsequenceTypesText = ` - ${negativeConsequenceTypes.length} terms filtered - `; - } - } - - return ` -
- ${positiveConsequenceTypes.join("
")} -
-
- ${negativeConsequenceTypesText} -
`; - } - return "-"; - } - - /* Usage: - columns: [ - { - title: "", classes: "", style: "", - columns: [ // nested column - { - title: "", classes: "", style: "" - } - ] - } - ] - - rows: [ - {values: ["", ""], classes: "", style: ""} - ] - */ - static renderTable(id, columns, rows, config) { - if (!rows || rows.length === 0) { - return `${config?.defaultMessage ? config.defaultMessage : "No data found"}`; - } - - let tr = ""; - const nestedColumnIndex = columns.findIndex(col => col.columns?.length > 0); - if (nestedColumnIndex > -1) { - let thTop = ""; - let thBottom = ""; - for (const column of columns) { - if (column.columns?.length > 0) { - thTop += `
`; - for (const bottomColumn of column.columns) { - thBottom += ``; - } - } else { - thTop += ``; - } - } - tr += `${thTop}`; - tr += `${thBottom}`; - } else { - const th = columns.map(column => ``).join(""); - tr = `${th}`; - } - - let html = `
${elem.title || elem.name}
${elem.title || elem.name}
${elem.title || elem.name}
- ${content} - + ${content} +
${column.title}${bottomColumn.title}${column.title}
${column.title}
- - ${tr} - - `; - // Render rows - for (const row of rows) { - let td = ""; - for (const value of row.values) { - td += ``; } - html += `${td}`; } - html += "
${value}
"; - return html; + // if (min < 10) { + // return `${description}`; + // } + // return "-"; + return min < 10 ? description : "-"; } - static _consequenceTypeDetailFormatterFilter(cts, filter) { - const selectedConsequenceTypes = []; - const notSelectedConsequenceTypes = []; - const showArrayIndexes = []; - - const geneSet = filter?.geneSet ? filter.geneSet : {}; - for (let i = 0; i < cts.length; i++) { - const ct = cts[i]; - - // Check if gene source is valid - let isSourceValid = false; - if (geneSet["ensembl"] && (!ct.source || ct.source.toUpperCase() === "ENSEMBL")) { // FIXME: Ensembl regulatory CT do not have 'source' - isSourceValid = true; - } else { - if (geneSet["refseq"] && ct.source?.toUpperCase() === "REFSEQ") { - isSourceValid = true; - } - } - if (!isSourceValid) { - // Not a valid source, let's continue to next ct - continue; - } - - // TODO Remove in IVA 2.3 - // To keep compatability with CellBase 4 - const transcriptFlags = ct.transcriptFlags ?? ct.transcriptAnnotationFlags; - let isCtSelected = filter.consequenceType?.all || false; - if (filter && isCtSelected === false) { - if (filter.consequenceType.maneTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("MANE Select")|| transcriptFlags?.includes("MANE Plus Clinical"); - } - if (filter.consequenceType.ensemblCanonicalTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("canonical"); - } - if (filter.consequenceType.gencodeBasicTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("basic"); - } - if (filter.consequenceType.ccdsTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("CCDS"); - } - if (filter.consequenceType.lrgTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("LRG"); - } - if (filter.consequenceType.ensemblTslTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("TSL:1"); - } - if (filter.consequenceType.illuminaTSO500Transcript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("TSO500"); - } - if (filter.consequenceType.eglhHaemoncTranscript) { - isCtSelected = isCtSelected || transcriptFlags?.includes("EGLH_HaemOnc"); - } - if (filter.consequenceType.proteinCodingTranscript && ct.biotype === "protein_coding") { - isCtSelected = isCtSelected || ct.biotype === "protein_coding"; - } - if (filter.consequenceType.highImpactConsequenceTypeTranscript) { - for (const so of ct.sequenceOntologyTerms) { - const impact = CONSEQUENCE_TYPES?.impact[so.name]?.toUpperCase(); - isCtSelected = isCtSelected || impact === "HIGH" || impact === "MODERATE"; + static polyphenProteinScoreFormatter(value, variant) { + let max = 0; + let description = ""; + if (variant?.annotation?.consequenceTypes?.length > 0) { + for (let i = 0; i < variant.annotation.consequenceTypes.length; i++) { + if (variant.annotation.consequenceTypes[i]?.proteinVariantAnnotation?.substitutionScores) { + for (let j = 0; j < variant.annotation.consequenceTypes[i].proteinVariantAnnotation.substitutionScores.length; j++) { + const substitutionScore = variant.annotation.consequenceTypes[i].proteinVariantAnnotation.substitutionScores[j]; + if (substitutionScore.source === "polyphen" && substitutionScore.score >= max) { + max = substitutionScore.score; + description = substitutionScore.description; + } } } } - // Check if the CT satisfy any condition - if (isCtSelected) { - showArrayIndexes.push(i); - selectedConsequenceTypes.push(ct); - } else { - notSelectedConsequenceTypes.push(ct); - } } - return { - selectedConsequenceTypes: selectedConsequenceTypes, - notSelectedConsequenceTypes: notSelectedConsequenceTypes, - indexes: showArrayIndexes - }; - } - static getHgvsLink(id, hgvsArray) { - if (!id) { - return; - } - - let hgvs = hgvsArray?.find(hgvs => hgvs.startsWith(id)); - if (hgvs) { - if (hgvs.includes("(")) { - const split = hgvs.split(new RegExp("[()]")); - hgvs = split[0] + split[2]; - } - - const split = hgvs.split(":"); - let link; - if (hgvs.includes(":c.")) { - link = BioinfoUtils.getTranscriptLink(split[0]); - } - if (hgvs.includes(":p.")) { - link = BioinfoUtils.getProteinLink(split[0]); - } - - return `${split[0]}:${split[1]}`; - } else { - if (id.startsWith("ENST") || id.startsWith("NM_") || id.startsWith("NR_")) { - return `${id}`; - } else { - return `${id}`; - } - } + // if (max > 0) { + // return `${description}`; + // } + // return "-"; + return max > 0 ? description : "-"; } - static toggleDetailConsequenceType(e) { - const id = e.target.dataset.id; - const elements = document.getElementsByClassName(this._prefix + id + "Filtered"); - for (const element of elements) { - if (element.style.display === "none") { - element.style.display = ""; - } else { - element.style.display = "none"; - } - } - } - - static consequenceTypeDetailFormatter(value, row, variantGrid, query, filter, assembly) { - if (row?.annotation?.consequenceTypes && row.annotation.consequenceTypes.length > 0) { - // Sort and group CTs by Gene name - BioinfoUtils.sort(row.annotation.consequenceTypes, v => v.geneName); - - const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, filter).indexes; - let message = ""; - if (filter) { - // Create two different divs to 'show all' or 'apply filter' title - message = `
- Showing ${showArrayIndexes.length} of - ${row.annotation.consequenceTypes.length} consequence types, - show all... -
- - `; - } - - let ctHtml = `
- ${message} -
- - - - - - - - - - - - - - - - - - - - - - `; - - for (let i = 0; i < row.annotation.consequenceTypes.length; i++) { - const ct = row.annotation.consequenceTypes[i]; - - // Keep backward compatibility with old ensemblGeneId and ensemblTranscriptId - const source = ct.source || "ensembl"; - const geneId = ct.geneId || ct.ensemblGeneId; - const transcriptId = ct.transcriptId || ct.ensemblTranscriptId; - const geneIdLink = `${BioinfoUtils.getGeneLink(geneId, source, assembly)}`; - const ensemblTranscriptIdLink = `${BioinfoUtils.getTranscriptLink(transcriptId, source, assembly)}`; - - // Prepare data info for columns - const geneName = ct.geneName ? `${ct.geneName}` : "-"; - - const geneIdLinkHtml = geneId ? `${geneId}` : ""; - const geneHtml = ` -
${geneName}
-
${geneIdLinkHtml}
- `; - - const transcriptIdHtml = ` -
- ${ct.biotype ? ct.biotype : "-"} -
-
- - ${transcriptId ? ` -
- ${VariantGridFormatter.getHgvsLink(transcriptId, row.annotation.hgvs) || ""} -
-
- ${VariantGridFormatter.getHgvsLink(ct?.proteinVariantAnnotation?.proteinId, row.annotation.hgvs) || ""} -
` : "" - } -
-
`; - - const soArray = []; - for (const so of ct.sequenceOntologyTerms) { - const color = CONSEQUENCE_TYPES.style[CONSEQUENCE_TYPES.impact[so.name]] || "black"; - const soUrl = `${BioinfoUtils.getSequenceOntologyLink(so.accession)}`; - const soTitle = `Go to Sequence Ontology ${so.accession} term`; - soArray.push(` -
- ${so.name} - - - -
- `); - } - - let transcriptFlags = ["-"]; - if (transcriptId && (ct.transcriptFlags?.length > 0 || ct.transcriptAnnotationFlags?.length > 0)) { - transcriptFlags = ct.transcriptFlags ? - ct.transcriptFlags.map(flag => `
${flag}
`) : - ct.transcriptAnnotationFlags.map(flag => `
${flag}
`); - } - - let exons = ["-"]; - if (ct.exonOverlap && ct.exonOverlap.length > 0) { - exons = ct.exonOverlap.map(exon => ` -
- ${exon.number} -
- ${exon?.percentage ? ` -
- ${exon?.percentage.toFixed(2) ?? "-"}% -
` : - ""} - `); - } - - let spliceAIScore = "-"; - if (ct.spliceScores?.length > 0) { - const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); - if (spliceAi) { - const keys = ["DS_AG", "DS_AL", "DS_DG", "DS_DL"]; - const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); - const index = [spliceAi.scores[keys[0]], spliceAi.scores[keys[1]], spliceAi.scores[keys[2]], spliceAi.scores[keys[3]]].findIndex(e => e === max); - - const color = (max >= 0.8) ? "red" : (max >= 0.5) ? "darkorange" : "black"; - spliceAIScore = ` -
- ${max} (${keys[index]}) -
- `; - } - } - - const pva = ct.proteinVariantAnnotation ? ct.proteinVariantAnnotation : {}; - let uniprotAccession = "-"; - if (pva.uniprotAccession) { - uniprotAccession = ` - - ${pva.uniprotVariantId ? ` -
- ${pva.uniprotVariantId} -
` : - ""} - `; - } - - let domains = ` - - `; - if (pva.features) { - let tooltipText = ""; - const visited = new Set(); - for (const feature of pva.features) { - if (feature.id && !visited.has(feature.id)) { - visited.add(feature.id); - tooltipText += ` -
- ${feature.id}${feature.description} -
- `; + static revelProteinScoreFormatter(value, variant) { + let max = 0; + if (variant?.annotation?.consequenceTypes?.length > 0) { + for (let i = 0; i < variant.annotation.consequenceTypes.length; i++) { + if (variant.annotation.consequenceTypes[i]?.proteinVariantAnnotation?.substitutionScores) { + for (let j = 0; j < variant.annotation.consequenceTypes[i].proteinVariantAnnotation.substitutionScores.length; j++) { + const substitutionScore = variant.annotation.consequenceTypes[i].proteinVariantAnnotation.substitutionScores[j]; + if (substitutionScore.source === "revel" && substitutionScore.score >= max) { + max = substitutionScore.score; } } - domains = ` - - `; } - - // Create the table row - const hideClass = showArrayIndexes.includes(i) ? "" : `${variantGrid._prefix}${row.id}Filtered`; - const displayStyle = showArrayIndexes.includes(i) ? "" : "display: none"; - ctHtml += ` - - - - - - - - - - - - - - - `; } - ctHtml += "
GeneTranscriptConsequence TypeTranscript FlagsTranscript Variant AnnotationProtein Variant Annotation
cDNA / CDSCodonExon (%)SpliceAIUniProt AccPositionRef/AltDomains
${geneHtml}${transcriptIdHtml}${soArray.join("")}${transcriptFlags.join("")}${ct.cdnaPosition || "-"} / ${ct.cdsPosition || "-"}${ct.codon || "-"}${exons.join("
")}
${spliceAIScore}${uniprotAccession}${pva.position !== undefined ? pva.position : "-"}${pva.reference !== undefined ? pva.reference + "/" + pva.alternate : "-"}${domains}
"; - return ctHtml; } - return "-"; + + // if (max > 0) { + // return `${max}`; + // } + // return "-"; + return max > 0 ? max : "-"; } - static caddScaledFormatter(value, row, index) { - if (row && row.type !== "INDEL" && row.annotation?.functionalScore?.length > 0) { - for (const functionalScore of row.annotation.functionalScore) { + static caddScaledFormatter(annotation, variant) { + let value; + if (variant?.type !== "INDEL" && variant.annotation?.functionalScore?.length > 0) { + for (const functionalScore of variant.annotation.functionalScore) { if (functionalScore.source === "cadd_scaled") { - const value = Number(functionalScore.score).toFixed(2); - if (value < 15) { - return value; - } else { - return "" + value + ""; - } + value = Number(functionalScore.score).toFixed(2); + // if (value < 15) { + // return value; + // } else { + // return "" + value + ""; + // } } } - } else { - return "-"; } + return value ?? "-"; } - static spliceAIFormatter(value, row) { - if (row.annotation.consequenceTypes?.length > 0) { + static spliceAIFormatter(value, variant) { + let dscore; + if (variant.annotation.consequenceTypes?.length > 0) { // We need to find the max Delta Score: // Delta score of a variant, defined as the maximum of (DS_AG, DS_AL, DS_DG, DS_DL), // ranges from 0 to 1 and can be interpreted as the probability of the variant being splice-altering. - let dscore = 0; + dscore = 0; let transcriptId; - for (const ct of row.annotation.consequenceTypes) { + for (const ct of variant.annotation.consequenceTypes) { if (ct.spliceScores?.length > 0) { const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); if (spliceAi) { @@ -618,175 +174,34 @@ export default class VariantFormatter { } } - const color = (dscore >= 0.8) ? "red" : (dscore >= 0.5) ? "darkorange" : "black"; - return ` -
- ${dscore} -
- `; - } else { - return "-"; + // const color = (dscore >= 0.8) ? "red" : (dscore >= 0.5) ? "darkorange" : "black"; + // return ` + //
+ // ${dscore} + //
+ // `; } + return dscore; } - static populationFrequenciesInfoTooltipContent(populationFrequencies) { - return `One coloured square is shown for each population. Frequencies are coded with colours which classify values - into 'very rare', 'rare', 'average', 'common' or 'missing', see - - https://www.nature.com/scitable/topicpage/multifactorial-inheritance-and-genetic-disease-919 - . Please, leave the cursor over each square to display the actual frequency values.
- Note that that all frequencies are percentages. -
-
Very rare: freq < 0.1 %
-
Rare: freq < 0.5 %
-
Average: freq < 5 %
-
Common: freq >= 5 %
-
Not observed
`; - } - - // Creates the colored table with one row and as many columns as populations. - static renderPopulationFrequencies(populations, populationFrequenciesMap, populationFrequenciesColor, populationFrequenciesConfig = {displayMode: "FREQUENCY_BOX"}) { - const tooltipRows = (populations || []).map(population => { - const popFreq = populationFrequenciesMap.get(population) || null; - const altFreq = popFreq?.altAlleleFreq?.toPrecision(4) || 0; - const altCount = popFreq?.altAlleleCount || 0; - const homAltFreq = popFreq?.altHomGenotypeFreq?.toPrecision(4) || 0; - const homAltCount = popFreq?.altHomGenotypeCount || 0; - const color = VariantGridFormatter._getPopulationFrequencyColor(altFreq, populationFrequenciesColor); - let altFreqText = ""; - let homAltFreqText = ""; - - // ALT freq tell us if the VARIANT has been OBSERVED. - if (altFreq > 0) { - altFreqText = `${altFreq || "-"} / ${altCount} (${altFreq > 0 ? (altFreq * 100).toPrecision(4) + "%" : "-"})`; - homAltFreqText = `${homAltFreq > 0 ? homAltFreq : "-"} / ${homAltCount} ${homAltFreq > 0 ? `(${(homAltFreq * 100).toPrecision(4)} %)` : ""}`; - } else { - altFreqText = "Not Observed"; - homAltFreqText = "Not Observed"; - } - - return ` - - - - - - ${altFreqText} - ${homAltFreqText} - - `; - }); - const tooltip = ` - - - - - - - - - ${tooltipRows.join("")} -
PopulationAllele ALT
(freq/count)
Genotype HOM_ALT
(freq/count)
- `; - - // Create the table (with the tooltip info) - let htmlPopFreqTable; - if (populationFrequenciesConfig?.displayMode === "FREQUENCY_BOX") { - const tableSize = populations.length * 15; - htmlPopFreqTable = ` - - - - `; - for (const population of populations) { - // This array contains "study:population" - let color = "black"; - if (typeof populationFrequenciesMap.get(population) !== "undefined") { - const freq = populationFrequenciesMap.get(population).altAlleleFreq || 0; - color = VariantGridFormatter._getPopulationFrequencyColor(freq, populationFrequenciesColor); - } - htmlPopFreqTable += ``; - } - htmlPopFreqTable += "
 
"; - } else { - htmlPopFreqTable = "
"; - const populationFrequenciesHtml = []; - for (const population of populations) { - let color = "black"; - if (typeof populationFrequenciesMap.get(population) !== "undefined") { // Freq exists - const freq = populationFrequenciesMap.get(population).altAlleleFreq || 0; - const percentage = (Number(freq) * 100).toPrecision(4); - // Only color the significant ones - if (freq <= 0.005) { - color = VariantGridFormatter._getPopulationFrequencyColor(freq, populationFrequenciesColor); - } - - if (populations.length > 1) { - populationFrequenciesHtml.push("
"); - populationFrequenciesHtml.push(`${population}`); - populationFrequenciesHtml.push(`${freq}`); - populationFrequenciesHtml.push(`(${percentage} %)`); - populationFrequenciesHtml.push("
"); - } else { - populationFrequenciesHtml.push("
"); - populationFrequenciesHtml.push(`${freq}`); - populationFrequenciesHtml.push(`(${percentage} %)`); - populationFrequenciesHtml.push("
"); - } - } else { // Freq does not exist - if (populations.length > 1) { - populationFrequenciesHtml.push("
"); - populationFrequenciesHtml.push(`${population}`); - populationFrequenciesHtml.push(`NA`); - populationFrequenciesHtml.push("
"); - } else { - populationFrequenciesHtml.push("
"); - populationFrequenciesHtml.push(`NA`); - populationFrequenciesHtml.push("
"); - } - } - } - htmlPopFreqTable += `${populationFrequenciesHtml.join("")}`; - htmlPopFreqTable += "
"; - } - - return htmlPopFreqTable; - } - - static _getPopulationFrequencyColor(freq, populationFrequenciesColor) { - let color; - if (freq === 0 || freq === "0") { - color = populationFrequenciesColor.unobserved; - } else if (freq < 0.001) { - color = populationFrequenciesColor.veryRare; - } else if (freq < 0.005) { - color = populationFrequenciesColor.rare; - } else if (freq < 0.05) { - color = populationFrequenciesColor.average; - } else { - color = populationFrequenciesColor.common; - } - return color; - } - - static clinicalTraitAssociationFormatter(value, row, index) { - const phenotypeHtml = ""; + static clinicalTraitAssociationFormatter(variant, source) { + // const phenotypeHtml = ""; // Check for ClinVar and Cosmic annotations - if (row?.annotation?.traitAssociation) { + if (variant?.annotation?.traitAssociation) { // Filter the traits for this column and check the number of existing traits - const traits = row.annotation.traitAssociation.filter(trait => trait.source.name.toUpperCase() === this.field.toUpperCase()); + const traits = variant.annotation.traitAssociation.filter(trait => trait.source.name.toUpperCase() === source.toUpperCase()); if (traits.length === 0) { - return ""; + // return ""; + return []; } + const results = []; let tooltipText = ""; - switch (this.field) { + switch (source.toLowerCase()) { case "clinvar": - const results = []; - const clinicalSignificanceVisited = new Set(); + // const clinicalSignificanceVisited = new Set(); for (const trait of traits) { - let clinicalSignificance, - drugResponseClassification; + let clinicalSignificance, drugResponseClassification; if (trait?.variantClassification?.clinicalSignificance) { clinicalSignificance = trait.variantClassification.clinicalSignificance; } else { @@ -839,39 +254,48 @@ export default class VariantFormatter { break; } - if (code !== "NP" && !clinicalSignificanceVisited.has(code)) { - results.push(`${code}`); - clinicalSignificanceVisited.add(code); - } + // if (code !== "NP" && !clinicalSignificanceVisited.has(code)) { + // results.push(`${code}`); + // clinicalSignificanceVisited.add(code); + // } // Prepare the tooltip links if (!trait.id?.startsWith("SCV")) { // We display the link plus the clinical significance and all the heritable trait descriptions - tooltipText += ` -
-
- ${trait.id} - - ${clinicalSignificance} ${drugResponseClassification ? "(" + drugResponseClassification + ")" : ""} - -
-
- ${trait?.heritableTraits?.length > 0 && trait.heritableTraits - .filter(t => t.trait && t.trait !== "not specified" && t.trait !== "not provided") - .map(t => `${t.trait}`) - .join("") - } -
-
`; + // tooltipText += ` + //
+ //
+ // ${trait.id} + // + // ${clinicalSignificance} ${drugResponseClassification ? "(" + drugResponseClassification + ")" : ""} + // + //
+ //
+ // ${trait?.heritableTraits?.length > 0 && trait.heritableTraits + // .filter(t => t.trait && t.trait !== "not specified" && t.trait !== "not provided") + // .map(t => `${t.trait}`) + // .join("") + // } + //
+ //
`; + + results.push({ + trait: trait, + clinicalSignificance: clinicalSignificance, + code: code, + color: color, + tooltip: tooltip + }); } } // This can only be shown if nothing else exists - if (results.length === 0) { - return "NP"; - } + // if (results.length === 0) { + // return "NP"; + // } - return `${results.join("
")}
`; + // return `${results.join("
")}
`; + return results; case "cosmic": // Prepare the tooltip links const cosmicMap = new Map(); @@ -884,28 +308,34 @@ export default class VariantFormatter { } }); - Array.from(cosmicMap.entries()).forEach(([traitId, histologies]) => { - const histologiesItems = Array.from(histologies.values()) - .filter(histology => histology && histology !== "null") - .map(histology => `${histology}`) - .join(""); - - tooltipText += ` -
- -
- ${histologiesItems} -
-
- `; - }); + Array.from(cosmicMap.entries()) + .forEach(([traitId, histologies]) => { + const histologies2 = Array.from(histologies.values()) + .filter(histology => histology && histology !== "null") + // .map(histology => `${histology}`) + // .join(""); + + // tooltipText += ` + //
+ //
+ // ${traitId} + //
+ //
+ // ${histologiesItems} + //
+ //
+ // `; + results.push({ + id: traitId, + histologies: histologies2 + }); + }); - return ` - - ${cosmicMap.size} ${cosmicMap.size > 1 ? "studies" : "study" } - `; + // return ` + // + // ${cosmicMap.size} ${cosmicMap.size > 1 ? "studies" : "study" } + // `; + return results; default: console.error("Wrong clinical source : " + this.field); break; @@ -914,350 +344,4 @@ export default class VariantFormatter { return phenotypeHtml; } - static clinicalCancerHotspotsFormatter(value, row) { - if (row?.annotation?.cancerHotspots?.length > 0) { - const cancerHotspotsHtml = new Map(); - for (const ct of row.annotation.consequenceTypes) { - for (const hotspot of row.annotation.cancerHotspots) { - if (ct.geneName === hotspot.geneName && ct.proteinVariantAnnotation?.position === hotspot.aminoacidPosition) { - cancerHotspotsHtml.set(`${hotspot.geneName}_${hotspot.aminoacidPosition}`, hotspot); - } - } - } - let tooltipText = ""; - for (const [key, hotspot] of cancerHotspotsHtml.entries()) { - tooltipText += ` -
-
- -
Cancer Type: ${hotspot.cancerType} - ${hotspot.variants.length} ${hotspot.variants.length === 1 ? "mutation" : "mutations"}
-
-
- ${ - hotspot.variants - .map(variant => ` - ${AMINOACID_CODE[hotspot.aminoacidReference]}${hotspot.aminoacidPosition}${AMINOACID_CODE[variant.aminoacidAlternate]}: ${variant.count} sample(s) - `) - .join("") - } -
-
`; - } - - if (cancerHotspotsHtml.size > 0) { - return ` - - ${cancerHotspotsHtml.size} ${cancerHotspotsHtml.size === 1 ? "variant" : "variants"} - `; - } - } - return ""; - } - - static clinicalTableDetail(value, row, index) { - const clinvar = []; - const cosmic = []; - const hotspots = []; - if (row.annotation?.traitAssociation?.length > 0) { - const cosmicIntermediate = new Map(); - for (const trait of row.annotation.traitAssociation) { - const values = []; - const vcvId = trait.additionalProperties.find(p => p.name === "VCV ID"); - const genomicFeature = trait.genomicFeatures.find(f => f.featureType.toUpperCase() === "GENE"); - const reviewStatus = trait.additionalProperties.find(p => p.name === "ReviewStatus_in_source_file"); - if (trait.source.name.toUpperCase() === "CLINVAR") { - values.push(`${trait.id}`); - values.push(vcvId ? vcvId.value : trait.id); - values.push(genomicFeature?.xrefs ? genomicFeature.xrefs?.symbol : "-"); - values.push(trait.variantClassification?.clinicalSignificance); - values.push(trait.consistencyStatus); - values.push(reviewStatus ? reviewStatus.value : "-"); - values.push(trait.heritableTraits ? trait.heritableTraits.map(t => t.trait).join("
") : "-"); - clinvar.push({ - values: values - }); - } else { // COSMIC section - // Prepare data to group by histologySubtype field - const key = trait.id + ":" + trait.somaticInformation.primaryHistology + ":" + trait.somaticInformation.primaryHistology; - const reviewStatus = trait.additionalProperties.find(p => p.id === "MUTATION_SOMATIC_STATUS"); - const zygosity = trait.additionalProperties.find(p => p.id === "MUTATION_ZYGOSITY"); - if (!cosmicIntermediate.has(key)) { - cosmicIntermediate.set(key, { - id: trait.id, - url: trait.url, - primarySite: trait.somaticInformation.primarySite, - primaryHistology: trait.somaticInformation.primaryHistology, - histologySubtypes: [], - histologySubtypesCounter: new Map(), - reviewStatus: reviewStatus, - pubmed: new Set(), - zygosity: new Set() - }); - } - // Only add the new terms for this key - if (trait.somaticInformation.histologySubtype) { - if (!cosmicIntermediate.get(key).histologySubtypesCounter.get(trait.somaticInformation.histologySubtype)) { - cosmicIntermediate.get(key).histologySubtypes.push(trait.somaticInformation.histologySubtype); - } - // Increment the counter always - cosmicIntermediate.get(key).histologySubtypesCounter - .set(trait.somaticInformation.histologySubtype, cosmicIntermediate.get(key).histologySubtypesCounter.size + 1); - } - if (trait?.bibliography?.length > 0) { - cosmicIntermediate.get(key).pubmed.add(...trait.bibliography); - } - if (zygosity) { - cosmicIntermediate.get(key).zygosity.add(zygosity.value); - } - } - } - - // Sort by key and prepare column data - for (const [key, c] of new Map([...cosmicIntermediate.entries()].sort())) { - const values = []; - values.push(`${c.id}`); - values.push(c.primarySite); - values.push(c.primaryHistology); - values.push(c.histologySubtypes - .map(value => { - if (cosmicIntermediate.get(key).histologySubtypesCounter.get(value) > 1) { - return value + " (x" + cosmicIntermediate.get(key).histologySubtypesCounter.get(value) + ")"; - } else { - return "-"; - } - }) - .join("
") || "-"); - values.push(Array.from(c.zygosity?.values()).join(", ") || "-"); - values.push(c?.reviewStatus?.value || "-"); - values.push(Array.from(c.pubmed.values()).map(p => `${p}`).join("
")); - cosmic.push({ - values: values - }); - } - } - - if (row?.annotation?.cancerHotspots?.length > 0) { - const visited = {}; - for (const ct of row.annotation.consequenceTypes) { - for (const hotspot of row.annotation.cancerHotspots) { - if (ct.geneName === hotspot.geneName && ct.proteinVariantAnnotation?.position === hotspot.aminoacidPosition && !visited[hotspot.geneName + "_" + hotspot.aminoacidPosition]) { - const reference = AMINOACID_CODE[hotspot.aminoacidReference]; - const position = hotspot.aminoacidPosition; - const values = []; - values.push(hotspot.geneName); - values.push(reference); - values.push(hotspot.aminoacidPosition); - values.push(hotspot.cancerType); - values.push(hotspot.variants.length); - values.push(hotspot.variants.map(m => `${reference}${position}${AMINOACID_CODE[m.aminoacidAlternate]}: ${m.count} sample(s)`).join("; ")); - hotspots.push({ - values: values - }); - visited[hotspot.geneName + "_" + hotspot.aminoacidPosition] = true; - } - } - } - } - - // Clinvar - const clinvarColumns = [ - {title: "ID"}, - {title: "Variation ID"}, - {title: "Gene"}, - {title: "Clinical Significance"}, - {title: "Consistency Status"}, - {title: "Review Status"}, - {title: "Traits"} - ]; - const clinvarTable = VariantGridFormatter.renderTable("", clinvarColumns, clinvar, {defaultMessage: "No ClinVar data found"}); - const clinvarTraits = ` -
- -
${clinvarTable}
-
`; - - // Cosmic - const cosmicColumns = [ - {title: "ID"}, - {title: "Primary Site"}, - {title: "Primary Histology"}, - {title: "Histology Subtype"}, - {title: "Zygosity"}, - {title: "Status"}, - {title: "Pubmed"} - ]; - const cosmicTable = VariantGridFormatter.renderTable("", cosmicColumns, cosmic, {defaultMessage: "No Cosmic data found"}); - const cosmicTraits = ` -
- -
${cosmicTable}
-
`; - - // Cancer Hotspots - const cancerHotspotsColumns = [ - {title: "Gene Name"}, - {title: "Aminoacid Reference"}, - {title: "Aminoacid Position"}, - {title: "Cancer Type"}, - {title: "Number of Mutations"}, - {title: "Mutations"}, - ]; - const cancerHotspotsTable = VariantGridFormatter.renderTable("", cancerHotspotsColumns, hotspots, {defaultMessage: "No Cancer Hotspots data found"}); - const cancerHotspotsHtml = ` -
- -
${cancerHotspotsTable}
-
`; - - return clinvarTraits + cosmicTraits + cancerHotspotsHtml; - } - - /* - * Reported Variant formatters - */ - static toggleDetailClinicalEvidence(e) { - const id = e.target.dataset.id; - const elements = document.getElementsByClassName(this._prefix + id + "EvidenceFiltered"); - for (const element of elements) { - if (element.style.display === "none") { - element.style.display = ""; - } else { - element.style.display = "none"; - } - } - } - - static reportedVariantFormatter(value, variant, index) { - return ` - ${variant?.interpretations?.length > 0 ? ` -
${variant.interpretations.length === 1 ? "1 case found" : `${variant.interpretations.length} cases found`}
-
-
REPORTED: ${variant.interpretationStats?.status?.REPORTED || 0} times
-
TIER 1: ${variant.interpretationStats?.tier?.TIER1 || 0} times
-
DISCARDED: ${variant.interpretationStats?.status?.DISCARDED || 0} times
-
` : ` -
No cases found
` - } - `; - } - - static reportedVariantDetailFormatter(value, row, opencgaSession) { - if (row?.interpretations?.length > 0) { - let reportedHtml = ` - - - - - - - - - - - - - - - - - - - - `; - - for (const interpretation of row.interpretations) { - // Prepare data info for columns - const caseId = interpretation.id.split(".")[0]; - const interpretationIdHtml = ` -
- -
- `; - - const panelsHtml = ` -
- ${interpretation.panels?.map(panel => { - if (panel?.source?.project === "PanelApp") { - return `${panel.name}`; - } else { - return `${panel.name || "-"}`; - } - })?.join("
")} -
`; - - const interpretedVariant = interpretation.primaryFindings.find(variant => variant.id === row.id); - - const sampleHtml = ` -
- -
`; - - const genotype = VariantInterpreterGridFormatter.alleleGenotypeRenderer(row, interpretedVariant.studies[0]?.samples[0], "call"); - const genotypeHtml = ` -
- ${genotype || "-"} -
`; - - const statusHtml = ` -
- ${interpretedVariant.status || "-"} -
`; - const discussionHtml = ` -
- ${interpretedVariant?.discussion?.text || "-"} -
`; - - const genes = []; - const transcripts = []; - const acmgClassifications = []; - const tierClassifications = []; - const clinicalSignificances = []; - for (const evidence of interpretedVariant.evidences.filter(ev => ev.review.select)) { - genes.push(` - - ${evidence.genomicFeature.geneName} - - `); - transcripts.push(` - - ${evidence.genomicFeature.transcriptId} - - `); - acmgClassifications.push(evidence.review.acmg?.map(acmg => acmg.classification)?.join(", ")|| "-"); - tierClassifications.push(evidence.review.tier || "-"); - clinicalSignificances.push(` - - ${evidence.review.clinicalSignificance || "-"} - - `); - } - - // Create the table row - reportedHtml += ` - - - - - - - - - - - - - - - `; - } - reportedHtml += "
CaseDisease PanelSampleGenotypeStatusDiscussionEvidences
GeneTranscriptACMGTierClinical Significance
${interpretationIdHtml}${interpretation?.panels?.length > 0 ? panelsHtml : "-"}${sampleHtml}${genotypeHtml}${statusHtml}${discussionHtml}${genes.length > 0 ? genes.join("
") : "-"}
${transcripts.length > 0 ? transcripts.join("
") : "-"}
${acmgClassifications.length > 0 ? acmgClassifications.join("
") : "-"}
${tierClassifications.length > 0 ? tierClassifications.join("
") : "-"}
${clinicalSignificances.length > 0 ? clinicalSignificances.join("
") : "-"}
"; - return reportedHtml; - } - return "-"; - } - } diff --git a/src/webcomponents/variant/variant-table-formatter.js b/src/webcomponents/variant/variant-table-formatter.js index b734c7ea94..d64ef7d43e 100644 --- a/src/webcomponents/variant/variant-table-formatter.js +++ b/src/webcomponents/variant/variant-table-formatter.js @@ -84,14 +84,12 @@ export default class VariantTableFormatter { }; } - static geneFormatter() { return { title: "Gene ID", - field: "id", - type: "basic", + type: "list", display: { - format: (id, variant) => { + getData: variant => { if (variant?.annotation?.consequenceTypes?.length > 0) { const visited = {}; const genes = []; @@ -105,9 +103,9 @@ export default class VariantTableFormatter { visited[geneName] = true; } } - return genes.join(", "); + return genes; } else { - return "-"; + return []; } }, // style: { @@ -117,30 +115,38 @@ export default class VariantTableFormatter { }; } - - static hgvsFormatter(variant, gridConfig) { - BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); - const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(variant.annotation?.consequenceTypes, gridConfig).indexes; - - if (showArrayIndexes?.length > 0 && variant.annotation.hgvs?.length > 0) { - const results = []; - for (const index of showArrayIndexes) { - const consequenceType = variant.annotation.consequenceTypes[index]; - const hgvsTranscriptIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.transcriptId)); - const hgvsProteingIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.proteinVariantAnnotation?.proteinId)); - if (hgvsTranscriptIndex > -1 || hgvsProteingIndex > -1) { - results.push(` -
- ${VariantGridFormatter.getHgvsLink(consequenceType.transcriptId, variant.annotation.hgvs) || "-"} -
-
- ${VariantGridFormatter.getHgvsLink(consequenceType.proteinVariantAnnotation?.proteinId, variant.annotation.hgvs) || "-"} -
- `); - } - } - return results.join("
"); - } + static hgvsFormatter(gridConfig) { + return { + title: "HGVS", + type: "list", + display: { + defaultValue: "-", + getData: variant => { + BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); + const showArrayIndexes = VariantTableFormatter._consequenceTypeDetailFormatterFilter(variant.annotation?.consequenceTypes, gridConfig).indexes; + + if (showArrayIndexes?.length > 0 && variant.annotation.hgvs?.length > 0) { + const results = []; + for (const index of showArrayIndexes) { + const consequenceType = variant.annotation.consequenceTypes[index]; + const transcriptHgvs = variant.annotation?.hgvs?.find(hgvs => hgvs.startsWith(consequenceType.transcriptId)); + const proteinHgvs = variant.annotation?.hgvs?.find(hgvs => hgvs.startsWith(consequenceType.proteinVariantAnnotation?.proteinId)); + + if (transcriptHgvs || proteinHgvs) { + results.push(transcriptHgvs); + proteinHgvs ? results.push(proteinHgvs) : results.push("-"); + } + } + return results; + } + }, + separator: hgvs => hgvs.includes("p.") || hgvs === "-" ? "---" : "" + // separator: "
" + // style: { + // "font-weight": "bold", + // } + }, + }; } static vcfFormatter(value, row, field, type = "INFO") { @@ -152,168 +158,106 @@ export default class VariantTableFormatter { } } - static typeFormatter(value, row) { - if (row) { - let type = row.type; - let color = ""; - switch (row.type) { - case "SNP": // Deprecated - type = "SNV"; - color = "black"; - break; - case "INDEL": - case "CNV": // Deprecated - case "COPY_NUMBER": - case "COPY_NUMBER_GAIN": - case "COPY_NUMBER_LOSS": - case "MNV": - color = "darkorange"; - break; - case "SV": - case "INSERTION": - case "DELETION": - case "DUPLICATION": - case "TANDEM_DUPLICATION": - case "BREAKEND": - color = "red"; - break; - default: - color = "black"; - break; - } - return `${type}`; - } else { - return "-"; - } - } - - static consequenceTypeFormatter(value, row, ctQuery, gridCtSettings) { - if (row?.annotation && row.annotation.consequenceTypes?.length > 0) { - let {selectedConsequenceTypes, notSelectedConsequenceTypes, indexes} = - VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, gridCtSettings); - - // If CT is passed in the query then we must make and AND with the selected transcript by the user. - // This means that only the selectedConsequenceTypes that ARE ALSO IN THE CT QUERY are displayed. - if (ctQuery) { - const consequenceTypes = new Set(); - for (const ct of ctQuery.split(",")) { - consequenceTypes.add(ct); - } - - const newSelectedConsequenceTypes = []; - for (const ct of selectedConsequenceTypes) { - if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { - newSelectedConsequenceTypes.push(ct); - } else { - notSelectedConsequenceTypes.push(ct); - } - } - selectedConsequenceTypes = newSelectedConsequenceTypes; - } - - const positiveConsequenceTypes = []; - const negativeConsequenceTypes = []; - const soVisited = new Set(); - for (const ct of selectedConsequenceTypes) { - for (const so of ct.sequenceOntologyTerms) { - if (!soVisited.has(so?.name)) { - positiveConsequenceTypes.push(`${so.name}`); - soVisited.add(so.name); - } - } - } - - // Print negative SO, if not printed as positive - let negativeConsequenceTypesText = ""; - if (gridCtSettings.consequenceType.showNegativeConsequenceTypes) { - for (const ct of notSelectedConsequenceTypes) { - for (const so of ct.sequenceOntologyTerms) { - if (!soVisited.has(so.name)) { - negativeConsequenceTypes.push(`
${so.name}
`); - soVisited.add(so.name); + static typeFormatter() { + return { + title: "Type", + field: "type", + type: "basic", + display: { + defaultValue: "-", + style: { + color: type => { + let color = ""; + switch (type) { + case "SNP": // Deprecated + // eslint-disable-next-line no-param-reassign + type = "SNV"; + color = "black"; + break; + case "INDEL": + case "CNV": // Deprecated + case "COPY_NUMBER": + case "COPY_NUMBER_GAIN": + case "COPY_NUMBER_LOSS": + case "MNV": + color = "darkorange"; + break; + case "SV": + case "INSERTION": + case "DELETION": + case "DUPLICATION": + case "TANDEM_DUPLICATION": + case "BREAKEND": + color = "red"; + break; + default: + color = "black"; + break; } - } - } - - if (negativeConsequenceTypes.length > 0) { - negativeConsequenceTypesText = ` - ${negativeConsequenceTypes.length} terms filtered - `; + return color; + }, } } - - return ` -
- ${positiveConsequenceTypes.join("
")} -
-
- ${negativeConsequenceTypesText} -
`; - } - return "-"; + }; } - /* Usage: - columns: [ - { - title: "", classes: "", style: "", - columns: [ // nested column - { - title: "", classes: "", style: "" - } - ] - } - ] + static consequenceTypeFormatter(ctQuery, gridSettings) { + return { + title: "Consequence Type", + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + getData: variant => { + if (variant.annotation?.consequenceTypes?.length > 0) { + let {selectedConsequenceTypes, notSelectedConsequenceTypes, indexes} = + VariantTableFormatter._consequenceTypeDetailFormatterFilter(variant.annotation.consequenceTypes, gridSettings); + + // If CT is passed in the query then we must make and AND with the selected transcript by the user. + // This means that only the selectedConsequenceTypes that ARE ALSO IN THE CT QUERY are displayed. + if (ctQuery) { + const consequenceTypes = new Set(); + for (const ct of ctQuery.split(",")) { + consequenceTypes.add(ct); + } - rows: [ - {values: ["", ""], classes: "", style: ""} - ] - */ - static renderTable(id, columns, rows, config) { - if (!rows || rows.length === 0) { - return `${config?.defaultMessage ? config.defaultMessage : "No data found"}`; - } + const newSelectedConsequenceTypes = []; + for (const ct of selectedConsequenceTypes) { + if (ct.sequenceOntologyTerms.some(so => consequenceTypes.has(so.name))) { + newSelectedConsequenceTypes.push(ct); + } else { + notSelectedConsequenceTypes.push(ct); + } + } + selectedConsequenceTypes = newSelectedConsequenceTypes; + } - let tr = ""; - const nestedColumnIndex = columns.findIndex(col => col.columns?.length > 0); - if (nestedColumnIndex > -1) { - let thTop = ""; - let thBottom = ""; - for (const column of columns) { - if (column.columns?.length > 0) { - thTop += `${column.title}`; - for (const bottomColumn of column.columns) { - thBottom += `${bottomColumn.title}`; + const positiveConsequenceTypes = []; + const soVisited = new Set(); + for (const ct of selectedConsequenceTypes) { + for (const so of ct.sequenceOntologyTerms) { + if (!soVisited.has(so?.name)) { + // positiveConsequenceTypes.push(`${so.name}`); + positiveConsequenceTypes.push(so.name); + soVisited.add(so.name); + } + } + } + + return positiveConsequenceTypes; } - } else { - thTop += `${column.title}`; + }, + style: { + // "font-weight": "bold", + "color": so => { + return CONSEQUENCE_TYPES.style[CONSEQUENCE_TYPES.impact[so]] || "black"; + }, } } - tr += `${thTop}`; - tr += `${thBottom}`; - } else { - const th = columns.map(column => `${column.title}`).join(""); - tr = `${th}`; - } - - let html = ` - - ${tr} - - `; - // Render rows - for (const row of rows) { - let td = ""; - for (const value of row.values) { - td += ``; - } - html += `${td}`; - } - html += "
${value}
"; - - return html; + }; } + static _consequenceTypeDetailFormatterFilter(cts, filter) { const selectedConsequenceTypes = []; const notSelectedConsequenceTypes = []; @@ -391,294 +335,211 @@ export default class VariantTableFormatter { }; } - static getHgvsLink(id, hgvsArray) { - if (!id) { - return; - } - - let hgvs = hgvsArray?.find(hgvs => hgvs.startsWith(id)); - if (hgvs) { - if (hgvs.includes("(")) { - const split = hgvs.split(new RegExp("[()]")); - hgvs = split[0] + split[2]; - } - - const split = hgvs.split(":"); - let link; - if (hgvs.includes(":c.")) { - link = BioinfoUtils.getTranscriptLink(split[0]); - } - if (hgvs.includes(":p.")) { - link = BioinfoUtils.getProteinLink(split[0]); - } - - return `${split[0]}:${split[1]}`; - } else { - if (id.startsWith("ENST") || id.startsWith("NM_") || id.startsWith("NR_")) { - return `${id}`; - } else { - return `${id}`; - } - } - } - - static toggleDetailConsequenceType(e) { - const id = e.target.dataset.id; - const elements = document.getElementsByClassName(this._prefix + id + "Filtered"); - for (const element of elements) { - if (element.style.display === "none") { - element.style.display = ""; - } else { - element.style.display = "none"; - } - } - } - - static consequenceTypeDetailFormatter(value, row, variantGrid, query, filter, assembly) { - if (row?.annotation?.consequenceTypes && row.annotation.consequenceTypes.length > 0) { - // Sort and group CTs by Gene name - BioinfoUtils.sort(row.annotation.consequenceTypes, v => v.geneName); - - const showArrayIndexes = VariantGridFormatter._consequenceTypeDetailFormatterFilter(row.annotation.consequenceTypes, filter).indexes; - let message = ""; - if (filter) { - // Create two different divs to 'show all' or 'apply filter' title - message = `
- Showing ${showArrayIndexes.length} of - ${row.annotation.consequenceTypes.length} consequence types, - show all... -
- - `; - } - - let ctHtml = `
- ${message} -
- - - - - - - - - - - - - - - - - - - - - - `; - - for (let i = 0; i < row.annotation.consequenceTypes.length; i++) { - const ct = row.annotation.consequenceTypes[i]; - - // Keep backward compatibility with old ensemblGeneId and ensemblTranscriptId - const source = ct.source || "ensembl"; - const geneId = ct.geneId || ct.ensemblGeneId; - const transcriptId = ct.transcriptId || ct.ensemblTranscriptId; - const geneIdLink = `${BioinfoUtils.getGeneLink(geneId, source, assembly)}`; - const ensemblTranscriptIdLink = `${BioinfoUtils.getTranscriptLink(transcriptId, source, assembly)}`; - - // Prepare data info for columns - const geneName = ct.geneName ? `${ct.geneName}` : "-"; - - const geneIdLinkHtml = geneId ? `${geneId}` : ""; - const geneHtml = ` -
${geneName}
-
${geneIdLinkHtml}
- `; - - const transcriptIdHtml = ` -
- ${ct.biotype ? ct.biotype : "-"} -
-
- - ${transcriptId ? ` -
- ${VariantGridFormatter.getHgvsLink(transcriptId, row.annotation.hgvs) || ""} -
-
- ${VariantGridFormatter.getHgvsLink(ct?.proteinVariantAnnotation?.proteinId, row.annotation.hgvs) || ""} -
` : "" + static siftFormatter() { + return { + title: "SIFT", + field: "annotation", + type: "basic", + display: { + defaultValue: "-", + format: (annotation, variant) => VariantFormatter.siftPproteinScoreFormatter(annotation, variant), + style: { + color: value => value !== "-" ? PROTEIN_SUBSTITUTION_SCORE.style.sift[value]: "black" } -
-
`; + }, + }; + } - const soArray = []; - for (const so of ct.sequenceOntologyTerms) { - const color = CONSEQUENCE_TYPES.style[CONSEQUENCE_TYPES.impact[so.name]] || "black"; - const soUrl = `${BioinfoUtils.getSequenceOntologyLink(so.accession)}`; - const soTitle = `Go to Sequence Ontology ${so.accession} term`; - soArray.push(` -
- ${so.name} - - - -
- `); + static polyphenFormatter() { + return { + title: "Polyphen", + field: "annotation", + type: "basic", + display: { + defaultValue: "-", + format: (annotation, variant) => VariantFormatter.polyphenProteinScoreFormatter(annotation, variant), + style: { + color: value => value !== "-" ? PROTEIN_SUBSTITUTION_SCORE.style.polyphen[value] : "black" } + }, + }; + } - let transcriptFlags = ["-"]; - if (transcriptId && (ct.transcriptFlags?.length > 0 || ct.transcriptAnnotationFlags?.length > 0)) { - transcriptFlags = ct.transcriptFlags ? - ct.transcriptFlags.map(flag => `
${flag}
`) : - ct.transcriptAnnotationFlags.map(flag => `
${flag}
`); + static revelFormatter() { + return { + title: "Revel", + field: "annotation", + type: "basic", + display: { + defaultValue: "-", + format: (annotation, variant) => VariantFormatter.revelProteinScoreFormatter(annotation, variant), + style: { + color: value => value > 0.5 ? "darkorange" : "black" } + } + }; + } - let exons = ["-"]; - if (ct.exonOverlap && ct.exonOverlap.length > 0) { - exons = ct.exonOverlap.map(exon => ` -
- ${exon.number} -
- ${exon?.percentage ? ` -
- ${exon?.percentage.toFixed(2) ?? "-"}% -
` : - ""} - `); + static caddScaledFormatter() { + return { + title: "CADD", + field: "annotation", + type: "basic", + display: { + defaultValue: "-", + format: (annotation, variant) => VariantFormatter.caddScaledFormatter(annotation, variant), + style: { + color: value => value < 15 ? "red" : "black", } + } + }; + } - let spliceAIScore = "-"; - if (ct.spliceScores?.length > 0) { - const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); - if (spliceAi) { - const keys = ["DS_AG", "DS_AL", "DS_DG", "DS_DL"]; - const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); - const index = [spliceAi.scores[keys[0]], spliceAi.scores[keys[1]], spliceAi.scores[keys[2]], spliceAi.scores[keys[3]]].findIndex(e => e === max); - - const color = (max >= 0.8) ? "red" : (max >= 0.5) ? "darkorange" : "black"; - spliceAIScore = ` -
- ${max} (${keys[index]}) -
- `; + static spliceAIFormatter() { + return { + title: "SpliceAI", + field: "annotation", + type: "basic", + display: { + defaultValue: "--", + format: annotation => { + if (annotation.consequenceTypes?.length > 0) { + let dscore = 0; + let transcriptId; + for (const ct of annotation.consequenceTypes) { + if (ct.spliceScores?.length > 0) { + const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); + if (spliceAi) { + const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); + if (max > dscore) { + dscore = max; + transcriptId = ct.transcriptId; + } + } + } + } + return dscore; } + }, + style: { + color: dscore => (dscore >= 0.8) ? "red" : (dscore >= 0.5) ? "darkorange" : "black", } + } + }; + } - const pva = ct.proteinVariantAnnotation ? ct.proteinVariantAnnotation : {}; - let uniprotAccession = "-"; - if (pva.uniprotAccession) { - uniprotAccession = ` - - ${pva.uniprotVariantId ? ` -
- ${pva.uniprotVariantId} -
` : - ""} - `; - } - - let domains = ` - - `; - if (pva.features) { - let tooltipText = ""; - const visited = new Set(); - for (const feature of pva.features) { - if (feature.id && !visited.has(feature.id)) { - visited.add(feature.id); - tooltipText += ` -
- ${feature.id}${feature.description} -
- `; + static conservationFormatter(source) { + return { + title: source, + field: "annotation", + type: "basic", + display: { + defaultValue: "-", + format: annotation => { + if (annotation?.conservation?.length > 0) { + for (const conservation of annotation.conservation) { + if (conservation.source?.toLowerCase() === source.toLowerCase()) { + return Number(conservation.score).toFixed(3); + } } } - domains = ` - - `; } - - // Create the table row - const hideClass = showArrayIndexes.includes(i) ? "" : `${variantGrid._prefix}${row.id}Filtered`; - const displayStyle = showArrayIndexes.includes(i) ? "" : "display: none"; - ctHtml += ` - - - - - - - - - - - - - - - `; } - ctHtml += "
GeneTranscriptConsequence TypeTranscript FlagsTranscript Variant AnnotationProtein Variant Annotation
cDNA / CDSCodonExon (%)SpliceAIUniProt AccPositionRef/AltDomains
${geneHtml}${transcriptIdHtml}${soArray.join("")}${transcriptFlags.join("")}${ct.cdnaPosition || "-"} / ${ct.cdsPosition || "-"}${ct.codon || "-"}${exons.join("
")}
${spliceAIScore}${uniprotAccession}${pva.position !== undefined ? pva.position : "-"}${pva.reference !== undefined ? pva.reference + "/" + pva.alternate : "-"}${domains}
"; - return ctHtml; - } - return "-"; + }; } - static caddScaledFormatter(value, row, index) { - if (row && row.type !== "INDEL" && row.annotation?.functionalScore?.length > 0) { - for (const functionalScore of row.annotation.functionalScore) { - if (functionalScore.source === "cadd_scaled") { - const value = Number(functionalScore.score).toFixed(2); - if (value < 15) { - return value; - } else { - return "" + value + ""; + static populationFrequencyFormatter() { + return { + title: "Population Frequency", + field: "annotation", + type: "table", + display: { + columns: [ + { + title: "", + field: "chromosome", + display: { + style: { + height: "10px", + width: "6px", + background: "blue", + } + } + }, + { + title: "", + field: "reference", + display: { + style: { + height: "10px", + width: "6px", + background: "red", + } + } + }, + { + title: "", + field: "alternate", + display: { + style: { + height: "10px", + width: "6px", + background: "green", + } + } } + ], + style: { + height: "10px" } } - } else { - return "-"; - } + }; } - static spliceAIFormatter(value, row) { - if (row.annotation.consequenceTypes?.length > 0) { - // We need to find the max Delta Score: - // Delta score of a variant, defined as the maximum of (DS_AG, DS_AL, DS_DG, DS_DL), - // ranges from 0 to 1 and can be interpreted as the probability of the variant being splice-altering. - let dscore = 0; - let transcriptId; - for (const ct of row.annotation.consequenceTypes) { - if (ct.spliceScores?.length > 0) { - const spliceAi = ct.spliceScores.find(ss => ss.source.toUpperCase() === "SPLICEAI"); - if (spliceAi) { - const max = Math.max(spliceAi.scores["DS_AG"], spliceAi.scores["DS_AL"], spliceAi.scores["DS_DG"], spliceAi.scores["DS_DL"]); - if (max > dscore) { - dscore = max; - transcriptId = ct.transcriptId; - } + static clinvarFormatter() { + return { + title: "ClinVar", + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + getData: variant => { + return VariantFormatter.clinicalTraitAssociationFormatter(variant, "clinvar"); + }, + template: "${trait.id} - ${clinicalSignificance}", + style: { + clinicalSignificance: { + color: (id, trait) => trait.color } + }, + link: { + "trait.id": (id, trait) => trait.trait.url } } + }; + } - const color = (dscore >= 0.8) ? "red" : (dscore >= 0.5) ? "darkorange" : "black"; - return ` -
- ${dscore} -
- `; - } else { - return "-"; - } + static cosmicFormatter() { + return { + title: "Cosmic", + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + getData: variant => { + return VariantFormatter.clinicalTraitAssociationFormatter(variant, "cosmic"); + }, + template: "${id} - ${histologies}", + style: { + // clinicalSignificance: { + // color: (id, trait) => trait.color + // } + }, + link: { + id: id => BioinfoUtils.getCosmicVariantLink(id) + } + } + }; } static populationFrequenciesInfoTooltipContent(populationFrequencies) { @@ -696,7 +557,7 @@ export default class VariantTableFormatter {
Not observed
`; } - // Creates the colored table with one row and as many columns as populations. +// Creates the colored table with one row and as many columns as populations. static renderPopulationFrequencies(populations, populationFrequenciesMap, populationFrequenciesColor, populationFrequenciesConfig = {displayMode: "FREQUENCY_BOX"}) { const tooltipRows = (populations || []).map(population => { const popFreq = populationFrequenciesMap.get(population) || null; From bd514eec3d49bd762c186532a7dd71b29fd86477 Mon Sep 17 00:00:00 2001 From: imedina Date: Thu, 23 Nov 2023 01:30:03 +0000 Subject: [PATCH 029/153] data-form: fix List separator --- src/webcomponents/commons/forms/data-form.js | 55 ++++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 95d6abc2d3..a1eb686f4b 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -1093,16 +1093,16 @@ export default class DataForm extends LitElement { _createListElement(element, data = this.data, section) { // Get values - let array; + let values; if (element.field) { - array = this.getValue(element.field, data); + values = this.getValue(element.field, data); } else { - array = element.display.getData(data); + values = element.display.getData(data); } const contentLayout = element.display?.contentLayout || "vertical"; // 1. Check array and layout exist - if (!Array.isArray(array)) { + if (!Array.isArray(values)) { return this._createElementTemplate(element, null, null, { message: this._getDefaultValue(element, section) ?? `Field '${element.field}' is not an array`, className: "text-danger" @@ -1117,14 +1117,14 @@ export default class DataForm extends LitElement { // 2. Apply 'filter' and 'transform' functions if defined if (typeof element.display?.filter === "function") { - array = element.display.filter(array); + values = element.display.filter(values); } if (typeof element.display?.transform === "function") { - array = element.display.transform(array); + values = element.display.transform(values); } // 3. Check length of the array. This MUST be done after filtering - if (array.length === 0) { + if (values.length === 0) { // If empty we just print the defaultValue, this is not an error return this._createElementTemplate(element, null, null, { message: this._getDefaultValue(element, section) ?? "Empty array", @@ -1133,17 +1133,16 @@ export default class DataForm extends LitElement { // 4. Format list elements. Initialise values with array, this is valid for scalars, or when 'template' and 'format' do not exist // Apply the template to all Array elements and store them in 'values' - let values = array; if (element.display?.format || element.display?.render) { // NOTE: 'element.display.render' is now deprecated, use 'format' instead if (element.display?.format) { - values = array.map(item => element.display.format(item, data)); + values = values.map(item => element.display.format(item, data)); } else { - values = array.map(item => element.display.render(item, data)); + values = values.map(item => element.display.render(item, data)); } } else { if (element.display?.template) { - values = array + values = values .map(item => this.applyTemplate(element.display.template, item, this._getDefaultValue(element, section), element)); } } @@ -1153,10 +1152,10 @@ export default class DataForm extends LitElement { if (element.display?.style) { if (typeof element.display.style === "string") { // All elements will have the same style - array.forEach(item => styles[item] = element.display.style); + values.forEach(item => styles[item] = element.display.style); } else { // It is an object, we must find the right style for each element - for (const item of array) { + for (const item of values) { // This call already checks if style is a function styles[item] = this._parseStyleField(element.display?.style, item, data); } @@ -1167,29 +1166,29 @@ export default class DataForm extends LitElement { const separators = {}; if (element.display?.separator) { // Last element cannot add a separator, so we iterate until length -1 - for (let i = 0; i < array.length - 1; i++) { + for (let i = 0; i < values.length - 1; i++) { let separator = null; if (typeof element.display.separator === "string") { separator = element.display.separator; } else { - separator = element.display.separator(array[i], i, array, data); - } - if (separator) { - separators[array[i]] = separator.includes("---") ? "
" : separator; + separator = element.display.separator(values[i], i, values, data); } + // if (separator) { + // separators[values[i]] = separator.includes("---") ? "
" : separator; + // } + separators[i] = separator.includes("---") ? "
" : separator; } } // 7. Render element values let content = this._getDefaultValue(element, section); - // const separator = element?.display?.separator; switch (contentLayout) { case "horizontal": content = ` ${values .map((elem, index) => ` ${elem} - ${index < array.length - 1 ? separators[elem] ?? ", " : ""} + ${index < values.length - 1 ? separators[index] ?? ", " : ""} `) .join(``) }`; @@ -1197,9 +1196,9 @@ export default class DataForm extends LitElement { case "vertical": content = ` ${values - .map(elem => ` + .map((elem, index) => `
${elem}
- ${separators[elem] ? `
${separators[elem]}
` : ""} + ${separators[index] ? `
${separators[index]}
` : ""} `) .join("") }`; @@ -1208,12 +1207,12 @@ export default class DataForm extends LitElement { content = `
    ${values - .map(elem => ` -
  • ${elem}
  • - ${separators[elem] ? `
    ${separators[elem]}
    ` : ""} - `) - .join("") - } + .map((elem, index) => ` +
  • ${elem}
  • + ${separators[index] ? `
    ${separators[index]}
    ` : ""} + `) + .join("") + }
`; break; From 6ad56394b37b32ec91485e2b6b5c4985395a4a2e Mon Sep 17 00:00:00 2001 From: imedina Date: Fri, 24 Nov 2023 00:49:47 +0000 Subject: [PATCH 030/153] data-form: add 'numbers' to List --- src/webcomponents/commons/forms/data-form.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index a1eb686f4b..14fa19f1d2 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -1108,7 +1108,7 @@ export default class DataForm extends LitElement { className: "text-danger" }); } - if (contentLayout !== "horizontal" && contentLayout !== "vertical" && contentLayout !== "bullets") { + if (contentLayout !== "horizontal" && contentLayout !== "vertical" && contentLayout !== "bullets" && contentLayout !== "numbers") { return this._createElementTemplate(element, null, null, { message: "Content layout must be 'horizontal', 'vertical' or 'bullets'", className: "text-danger" @@ -1216,6 +1216,19 @@ export default class DataForm extends LitElement { `; break; + case "numbers": + content = ` +
    + ${values + .map((elem, index) => ` +
  1. ${elem}
  2. + ${separators[index] ? `
    ${separators[index]}
    ` : ""} + `) + .join("") + } +
+ `; + break; } return this._createElementTemplate(element, null, content); From 56dcea6876db493e8ccf65d89eb32422cceee368 Mon Sep 17 00:00:00 2001 From: imedina Date: Fri, 24 Nov 2023 16:17:58 +0000 Subject: [PATCH 031/153] pdf-builder initial work --- .../clinical/clinical-analysis-review.js | 2 +- src/webcomponents/cohort/cohort-view.js | 2 +- .../commons/forms}/pdf-builder.js | 224 +++++++++++------- src/webcomponents/sample/sample-view.js | 2 +- 4 files changed, 147 insertions(+), 83 deletions(-) rename src/{core => webcomponents/commons/forms}/pdf-builder.js (66%) diff --git a/src/webcomponents/clinical/clinical-analysis-review.js b/src/webcomponents/clinical/clinical-analysis-review.js index 37e57eb5a7..f110ac2073 100644 --- a/src/webcomponents/clinical/clinical-analysis-review.js +++ b/src/webcomponents/clinical/clinical-analysis-review.js @@ -21,7 +21,7 @@ import LitUtils from "../commons/utils/lit-utils.js"; import ClinicalAnalysisManager from "./clinical-analysis-manager.js"; import FormUtils from "../commons/forms/form-utils.js"; import NotificationUtils from "../commons/utils/notification-utils.js"; -import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; +// import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; import "./clinical-analysis-summary.js"; import "../variant/interpretation/variant-interpreter-grid.js"; import "../disease-panel/disease-panel-grid.js"; diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index 49fedb715f..b255914796 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -18,7 +18,7 @@ import {LitElement, html} from "lit"; import UtilsNew from "../../core/utils-new.js"; import LitUtils from "../commons/utils/lit-utils.js"; import Types from "../commons/types.js"; -import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; +import PdfBuilder, {stylePdf} from "../commons/forms/pdf-builder.js"; import "../commons/forms/data-form.js"; import "../loading-spinner.js"; import "../study/annotationset/annotation-set-view.js"; diff --git a/src/core/pdf-builder.js b/src/webcomponents/commons/forms/pdf-builder.js similarity index 66% rename from src/core/pdf-builder.js rename to src/webcomponents/commons/forms/pdf-builder.js index ad8a213092..07e097ba6f 100644 --- a/src/core/pdf-builder.js +++ b/src/webcomponents/commons/forms/pdf-builder.js @@ -1,85 +1,28 @@ -import UtilsNew from "./utils-new"; +import UtilsNew from "../../../core/utils-new.js"; -export default class PdfBuilder { - - defaultStyles = { - h1: { - fontSize: 24, - bold: true, - }, - h2: { - fontSize: 20, - bold: true, - }, - h3: { - fontSize: 18, - bold: true, - }, - h4: { - fontSize: 16, - bold: true, - }, - subheader: { - fontSize: 14, - bold: true, - }, - body: { - fontSize: 12, - }, - label: { - fontSize: 12, - bold: true - }, - caption: { - fontSize: 8 - }, - note: { - fontSize: 10 - } - }; - defaultTableLayout = { - headerVerticalBlueLine: { - // top & bottom - hLineWidth: function () { - return 0; - }, - // left & right - vLineWidth: function (i, node) { - // i == 0 mean no draw line on start - // i == node.table.body.length no draw the last line - if (i === node.table.body.length) { - return 0; - } - // it will draw a line if i == 0 - return i === 0 ? 2 : 0; - }, - vLineColor: function (i) { - return i === 0 ? "#0c2f4c" : ""; - }, - fillColor: function () { - return "#f3f3f3"; - } - }, - }; +export default class PdfBuilder { - constructor(data, docDefinition) { - this.#init(data, docDefinition); + constructor(data, dataFormConfig, pdfConfig) { + this.#init(data, dataFormConfig, pdfConfig); } // docDefinition Config - #init(data, dataFormConfig) { + #init(data, dataFormConfig, pdfConfig) { this.data = data ?? {}; - this.docDefinitionConfig = dataFormConfig; + this.dataFormConfig = dataFormConfig; + this.pdfConfig = {...this.getDefaultConfig(), ...pdfConfig}; + this.docDefinition = { - pageSize: "A4", - styles: {...this.defaultStyles, ...dataFormConfig?.styles}, - defaultStyle: { - fontSize: 10 - }, - watermark: {...dataFormConfig?.displayDoc.watermark}, - content: [this.#creatTextElement(dataFormConfig?.displayDoc?.headerTitle), dataFormConfig?.sections ? this.#transformData() : []] + // pageSize: "A4", + // styles: {...this.pdfConfig?.styles, ...pdfConfig?.styles}, + // defaultStyle: { + // ...this.pdfConfig?.defaultStyle + // }, + // watermark: {...dataFormConfig?.displayDoc.watermark}, + ...this.pdfConfig, + content: [] }; } @@ -99,14 +42,68 @@ export default class PdfBuilder { return new Promise((resolve, reject) => { pdfMake.createPdf(this.doc, this.table) .getBlob(result =>{ - resolve(result); - }, - err =>{ - reject(err); - }); + resolve(result); + }, + err =>{ + reject(err); + }); }); } + render() { + const style = this._parseStyleField(this.config?.display?.style); + + // if (this.config?.display?.layout && Array.isArray(this.config.display.layout)) { + // // Render with a specific layout + // return html` + //
+ // ${this.config?.display.layout + // .map(section => { + // const sectionClassName = section.className ?? section.classes ?? ""; + // const sectionStyle = section.style ?? ""; + // + // if (section.id) { + // return html` + //
+ // ${this._createSection(this.config.sections.find(s => s.id === section.id))} + //
+ // `; + // } else { + // // this section contains nested subsections: 'sections' + // return html` + //
+ // ${(section.sections || []) + // .map(subsection => { + // const subsectionClassName = subsection.className ?? subsection.classes ?? ""; + // const subsectionStyle = this._parseStyleField(subsection.style); + // if (subsection.id) { + // return html` + //
+ // ${this._createSection(this.config.sections.find(s => s.id === subsection.id))} + //
+ // `; + // } else { + // return nothing; + // } + // }) + // } + //
+ // `; + // } + // })} + //
+ // `; + // } else { + // // Render without layout + // return html` + //
+ // ${this.config.sections.map(section => this._createSection(section))} + //
+ // `; + // } + + } + #getBooleanValue(value, defaultValue) { const _defaultValue = typeof defaultValue !== "undefined" ? defaultValue : true; @@ -136,7 +133,7 @@ export default class PdfBuilder { * @param {string} field - * @param {string} defaultValue -- * @returns {Any} return style - */ + */ #getValue(field, defaultValue) { const _value = UtilsNew.getObjectValue(this.data, field, defaultValue); if (typeof _value === "boolean") { @@ -368,6 +365,73 @@ export default class PdfBuilder { return htmlToPdfmake(container, {...defaultHtmlStyle}); } + getDefaultConfig() { + return { + defaultStyle: { + fontSize: 10, + }, + styles: { + h1: { + fontSize: 24, + bold: true, + }, + h2: { + fontSize: 20, + bold: true, + }, + h3: { + fontSize: 18, + bold: true, + }, + h4: { + fontSize: 16, + bold: true, + }, + subheader: { + fontSize: 14, + bold: true, + }, + body: { + fontSize: 12, + }, + label: { + fontSize: 12, + bold: true + }, + caption: { + fontSize: 8 + }, + note: { + fontSize: 10 + } + }, + defaultTableLayout: { + headerVerticalBlueLine: { + // top & bottom + hLineWidth: function () { + return 0; + }, + // left & right + vLineWidth: function (i, node) { + // i == 0 mean no draw line on start + // i == node.table.body.length no draw the last line + if (i === node.table.body.length) { + return 0; + } + // it will draw a line if i == 0 + return i === 0 ? 2 : 0; + }, + vLineColor: function (i) { + return i === 0 ? "#0c2f4c" : ""; + }, + fillColor: function () { + return "#f3f3f3"; + } + } + } + }; + } + } // https://pdfmake.github.io/docs/0.1/document-definition-object/styling/#style-properties @@ -391,7 +455,7 @@ export default class PdfBuilder { /** * @param {Style} style - Define the style config you want to use for the element * @returns {Style} return style -*/ + */ export function stylePdf(style) { return {...style}; } diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 5ffc74ef85..cb71089a18 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -23,7 +23,7 @@ import "../commons/forms/data-form.js"; import "../commons/filters/catalog-search-autocomplete.js"; import "../study/annotationset/annotation-set-view.js"; import "../loading-spinner.js"; -import PdfBuilder, {stylePdf} from "../../core/pdf-builder.js"; +import PdfBuilder, {stylePdf} from "../commons/forms/pdf-builder"; import CatalogGridFormatter from "../commons/catalog-grid-formatter"; export default class SampleView extends LitElement { From 9d02736864b13a191ae9b89896aa68930522f875 Mon Sep 17 00:00:00 2001 From: imedina Date: Fri, 1 Dec 2023 15:37:05 +0000 Subject: [PATCH 032/153] pdf: print Lists in PDF --- src/sites/test-app/index.html | 6 +- src/webcomponents/cohort/cohort-view.js | 1 + src/webcomponents/commons/forms/data-form.js | 18 +- .../commons/forms/pdf-builder.js | 728 ++++++++++++++++-- .../individual/individual-view.js | 1 + 5 files changed, 672 insertions(+), 82 deletions(-) diff --git a/src/sites/test-app/index.html b/src/sites/test-app/index.html index cf61e54d28..cc2371388d 100644 --- a/src/sites/test-app/index.html +++ b/src/sites/test-app/index.html @@ -85,9 +85,13 @@ - + + + + + diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index b255914796..5725e7de0d 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -67,6 +67,7 @@ export default class CohortView extends LitElement { titleVisible: false, titleWidth: 2, defaultValue: "-", + pdf: true }; this._config = this.getDefaultConfig(); } diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 14fa19f1d2..fa02042f21 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -28,6 +28,7 @@ import "../forms/text-field-filter.js"; import "./toggle-switch.js"; import "./toggle-buttons.js"; import "../data-table.js"; +import PdfBuilder from "./pdf-builder.js"; export default class DataForm extends LitElement { @@ -2017,9 +2018,9 @@ export default class DataForm extends LitElement { LitUtils.dispatchCustomEvent(this, "submit", section, {}, null); } - onCustomEvent(e, eventName, data) { - LitUtils.dispatchCustomEvent(this, eventName, data); - } + // onCustomEvent(e, eventName, data) { + // LitUtils.dispatchCustomEvent(this, eventName, data); + // } onSectionChange(e) { e.preventDefault(); @@ -2105,6 +2106,11 @@ export default class DataForm extends LitElement { } } + onDownloadPdf() { + const pdfDocument = new PdfBuilder(this.data, this.config); + pdfDocument.exportToPdf(); + } + renderContentAsForm() { // Buttons values const buttonsVisible = this._getBooleanValue(this.config.display?.buttonsVisible ?? this.config.buttons?.show, true); @@ -2133,6 +2139,12 @@ export default class DataForm extends LitElement { ` : null } + + ${buttonsVisible && buttonsLayout?.toUpperCase() === "TOP" ? this.renderButtons(null) : null} diff --git a/src/webcomponents/commons/forms/pdf-builder.js b/src/webcomponents/commons/forms/pdf-builder.js index 07e097ba6f..0bb92b85c7 100644 --- a/src/webcomponents/commons/forms/pdf-builder.js +++ b/src/webcomponents/commons/forms/pdf-builder.js @@ -1,5 +1,6 @@ - -import UtilsNew from "../../../core/utils-new.js"; +// import pdfMake from "pdfmake/build/pdfmake"; +// import pdfFonts from "pdfmake/build/vfs_fonts"; +// pdfMake.vfs = pdfFonts.pdfMake.vfs; export default class PdfBuilder { @@ -12,7 +13,7 @@ export default class PdfBuilder { #init(data, dataFormConfig, pdfConfig) { this.data = data ?? {}; this.dataFormConfig = dataFormConfig; - this.pdfConfig = {...this.getDefaultConfig(), ...pdfConfig}; + this.pdfConfig = {...this.getDefaultPdfConfig(), ...pdfConfig}; this.docDefinition = { // pageSize: "A4", @@ -24,6 +25,7 @@ export default class PdfBuilder { ...this.pdfConfig, content: [] }; + this.render(); } exportToPdf() { @@ -50,96 +52,660 @@ export default class PdfBuilder { }); } - render() { - const style = this._parseStyleField(this.config?.display?.style); - - // if (this.config?.display?.layout && Array.isArray(this.config.display.layout)) { - // // Render with a specific layout - // return html` - //
- // ${this.config?.display.layout - // .map(section => { - // const sectionClassName = section.className ?? section.classes ?? ""; - // const sectionStyle = section.style ?? ""; - // - // if (section.id) { - // return html` - //
- // ${this._createSection(this.config.sections.find(s => s.id === section.id))} - //
- // `; - // } else { - // // this section contains nested subsections: 'sections' - // return html` - //
- // ${(section.sections || []) - // .map(subsection => { - // const subsectionClassName = subsection.className ?? subsection.classes ?? ""; - // const subsectionStyle = this._parseStyleField(subsection.style); - // if (subsection.id) { - // return html` - //
- // ${this._createSection(this.config.sections.find(s => s.id === subsection.id))} - //
- // `; - // } else { - // return nothing; - // } - // }) - // } - //
- // `; - // } - // })} - //
- // `; - // } else { - // // Render without layout - // return html` - //
- // ${this.config.sections.map(section => this._createSection(section))} - //
- // `; - // } + + + #getVisibleSections() { + return this.docDefinitionConfig.sections + .filter(section => section?.elements[0]?.type !== "notification" || section?.elements?.length > 1) + .filter(section => this.#getBooleanValue(section?.display?.visible, true) && this.#getBooleanValue(section?.display?.showPDF, true)); } - #getBooleanValue(value, defaultValue) { - const _defaultValue = typeof defaultValue !== "undefined" ? defaultValue : true; + /* + * @param {string} field - + * @param {string} defaultValue -- + * @returns {Any} return style + */ + // #getValue(field, defaultValue) { + // const _value = UtilsNew.getObjectValue(this.data, field, defaultValue); + // if (typeof _value === "boolean") { + // return _value.toString(); + // } + // return _value; + // } - if (typeof value !== "undefined" && value !== null) { + #getValue(field, object = this.data, defaultValue, display) { + let value; + if (field) { + // Optional chaining is needed when "res" is undefined + value = field.split(".").reduce((res, prop) => res?.[prop], object); - if (typeof value === "boolean") { - return value; + // If 'value' exists we must apply the functions, DO NOT change the order + if (value) { + if (display?.format) { + // Check if response is actually an HTML + // value = UtilsNew.renderHTML(display.format(value)); + value = display.format(value, object); + } + // if (display?.link) { + // const href = display.link.replace(field.toUpperCase(), value); + // value = html`${value}`; + // } + // if (display?.className || display?.classes || display?.style) { + // const style = this._parseStyleField(display.style, value, object); + // value = html`${value}`; + // } + } else { + value = defaultValue; } + } else { + value = defaultValue; + } + return value; + } - if (typeof value === "function") { - return value(this.data); + #getBooleanValue(value, defaultValue) { + let _value = typeof defaultValue !== "undefined" ? defaultValue : true; + if (typeof value !== "undefined" && value !== null) { + if (typeof value === "boolean") { + _value = value; } else { - console.error(`Expected boolean or function value, but got ${typeof value}`); + if (typeof value === "function") { + _value = value(this.data); + } else { + console.error(`Expected boolean or function value, but got '${typeof value}'`); + } } } - return _defaultValue; + return _value; + } + #getDefaultValue(element, section) { + // Preference order: element, section and then global config + return element?.display?.defaultValue ?? section?.display?.defaultValue ?? this.dataFormConfig?.display?.defaultValue ?? ""; + } + #getDefaultLayout(element, section) { + return element?.display?.defaultLayout ?? section?.display?.defaultLayout ?? this.dataFormConfig?.display?.defaultLayout ?? "horizontal"; } - #getVisibleSections() { - return this.docDefinitionConfig.sections - .filter(section => section?.elements[0]?.type !== "notification" || section?.elements?.length > 1) - .filter(section => this.#getBooleanValue(section?.display?.visible, true) && this.#getBooleanValue(section?.display?.showPDF, true)); + #getElementWidth(element, section) { + return element?.display?.width ?? section?.display?.width ?? this.dataFormConfig?.display?.width ?? null; } - /** - * @param {string} field - - * @param {string} defaultValue -- - * @returns {Any} return style - */ - #getValue(field, defaultValue) { - const _value = UtilsNew.getObjectValue(this.data, field, defaultValue); - if (typeof _value === "boolean") { - return _value.toString(); + #getElementTitleWidth(element, section) { + return element?.display?.titleWidth ?? section?.display?.titleWidth ?? this.dataFormConfig?.display?.titleWidth ?? null; + } + + applyTemplate(template, data, defaultValue, element) { + // Parse template string and find matches groups + const matches = template + .match(/\$\{[a-zA-Z_.\[\]]+}/g) + .map(elem => elem.substring(2, elem.length - 1)); + + const content = { + columnGap: 5, + columns: [] + }; + for (const match of matches) { + // Check if 'style' has been defined for this match variable, example: + // { + // title: "Format", + // type: "complex", + // display: { + // template: "${format} (${bioformat})", + // format: { + // "format": value => value.toUpperCase() + // }, + // style: { + // format: { + // "font-weight": "bold", + // "color": "red" + // }, + // bioformat: { + // "color": "green" + // } + // }, + // link: { + // format: (value, data) => https://... + // } + // }, + // }, + const value = {text: this.#getValue(match, data, defaultValue)}; + if (element?.display?.format?.[match]) { + value.text = element?.display?.format?.[match](value, data); + } + if (element?.display?.link?.[match]) { + value.link = element?.display?.link?.[match](value, data); + } + if (element?.display?.style?.[match] && typeof element.display.style[match] === "object") { + if (element.display.style[match].color) { + value.color = element.display.style[match].color; + } + if (element.display.style[match]["font-weight"]) { + value.bold = element.display.style[match]["font-weight"] === "bold"; + } + } + content.columns.push( + { + ...value, + width: "auto" + } + ); } - return _value; + + return content; + } + + render() { + + if (this.dataFormConfig?.display?.layout && Array.isArray(this.dataFormConfig.display.layout)) { + // Render with a specific layout + // return html` + //
+ // ${this.dataFormConfig?.display.layout + // .map(section => { + // const sectionClassName = section.className ?? section.classes ?? ""; + // const sectionStyle = section.style ?? ""; + // + // if (section.id) { + // return html` + //
+ // ${this._createSection(this.dataFormConfig.sections.find(s => s.id === section.id))} + //
+ // `; + // } else { + // // this section contains nested subsections: 'sections' + // return html` + //
+ // ${(section.sections || []) + // .map(subsection => { + // const subsectionClassName = subsection.className ?? subsection.classes ?? ""; + // const subsectionStyle = this._parseStyleField(subsection.style); + // if (subsection.id) { + // return html` + //
+ // ${this._createSection(this.dataFormConfig.sections.find(s => s.id === subsection.id))} + //
+ // `; + // } else { + // return nothing; + // } + // }) + // } + //
+ // `; + // } + // })} + //
+ // `; + } else { + // Render without layout + // return html` + //
+ // ${this.dataFormConfig.sections.map(section => this._createSection(section))} + //
+ // `; + const content = this.dataFormConfig.sections.map(section => this._createSection(section)); + // debugger + this.docDefinition.content.push(...content); + } + + } + + _createSection(section) { + // Check if the section is visible + if (section.display && !this.#getBooleanValue(section.display.visible)) { + return; + } + + // Section values + // const sectionClassName = section?.display?.className ?? section?.display?.classes ?? ""; + // const sectionStyle = section?.display?.style ?? ""; + // const sectionWidth = "col-md-" + this._getSectionWidth(section); + // + // // Section title values + // const titleHeader = section?.display?.titleHeader ?? "h3"; + // const titleClassName = section?.display?.titleClassName ?? section?.display?.titleClasses ?? ""; + // const titleStyle = section?.display?.titleStyle ?? ""; + // + // // Section description values + // const description = section.description ?? section.text ?? null; + // const descriptionClassName = section.display?.descriptionClassName ?? "help-block"; + // const descriptionStyle = section.display?.descriptionStyle ?? section.display?.textStyle ?? ""; + // + // const buttonsVisible = this._getBooleanValue(section.display?.buttonsVisible ?? false); + + let content; + // Check if a custom layout has been provided + if (section.display?.layout && Array.isArray(section.display.layout)) { + // Render with a specific layout + // content = html` + //
+ // ${section.display.layout + // .map(element => { + // const elementClassName = element.className ?? element.classes ?? ""; + // const elementStyle = element.style ?? ""; + // + // if (element.id) { + // return html` + //
+ // ${this._createElement(section.elements.find(s => s.id === element.id))} + //
+ // `; + // } else { + // // this section contains nested subsections: 'sections' + // return html` + //
+ // ${(element.elements || []) + // .map(subelement => { + // const subsectionClassName = subelement.className ?? subelement.classes ?? ""; + // const subsectionStyle = this._parseStyleField(subelement.style); + // if (subelement.id) { + // return html` + //
+ // ${this._createElement(section.elements.find(s => s.id === subelement.id))} + //
+ // `; + // } else { + // return nothing; + // } + // }) + // } + //
+ // `; + // } + // })} + //
+ // `; + } else { + // Otherwise render vertically + // content = html` + //
+ // ${section.elements.map(element => this._createElement(element, section))} + //
+ // `; + content = section.elements.map(element => this._createElement(element, section)); + } + + // return html` + //
+ //
+ // ${section.title ? html` + //
+ // ${this._getTitleHeader(titleHeader, section.title, titleClassName, titleStyle)} + //
+ // ` : null} + // ${description ? html` + //
+ //
+ // ${description} + //
+ //
+ // ` : null} + // ${content} + //
+ //
+ // ${buttonsVisible ? this.renderButtons(null, section?.id) : null} + // `; + return content; + } + + _createElement(element, section) { + // Check if the element is visible + if (element.display && !this.#getBooleanValue(element.display.visible, true)) { + return; + } + + // Check if type is 'separator', this is a special case, no need to parse 'name' and 'content' + // if (element.type === "separator") { + // return html`
`; + // } + + // To store element content + let content = ""; + + // if not 'type' is defined we assumed is 'basic' and therefore field exist + if (!element.type || element.type === "basic") { + // content = html`${this.getValue(element.field, this.data, this._getDefaultValue(element, section), element.display)}`; + content = {text: this.#getValue(element.field, this.data, this.#getDefaultValue(element, section), element.display)}; + } else { + // Other 'type' are rendered by specific functions + switch (element.type) { + // View elements + // case "text": + // case "title": + // case "notification": + // content = this._createTextElement(element); + // break; + case "complex": + content = this._createComplexElement(element, this.data, section); + break; + case "list": + content = this._createListElement(element, this.data, section); + break; + case "table": + // content = this._createTableElement(element, this.data, section); + // break; + // case "image": + // content = this._createImageElement(element); + // break; + // case "chart": + // case "plot": + // content = this._createPlotElement(element); + // break; + // case "json": + // content = this._createJsonElement(element, section); + // break; + // case "tree": + // content = this._createTreeElement(element); + // break; + // case "download": + // content = this._createDownloadElement(element); + // break; + // case "custom": + // content = html`${this._createCustomElement(element)}`; + // break; + + // Form controls and editors + // case "input-text": + // content = this._createInputElement(element, "text", section); + // break; + // case "input-num": + // content = this._createInputElement(element, "number", section); + // break; + // case "input-password": + // content = this._createInputElement(element, "password", section); + // break; + // case "input-number": + // content = this._createInputNumberElement(element, section); + // break; + // case "input-date": + // content = this._createInputDateElement(element, section); + // break; + // case "checkbox": + // content = this._createCheckboxElement(element); + // break; + // case "select": + // content = this._createInputSelectElement(element); + // break; + // case "toggle-switch": + // content = this._createToggleSwitchElement(element); + // break; + // case "toggle-buttons": + // content = this._createToggleButtonsElement(element); + // break; + // case "json-editor": + // content = this._createJsonEditorElement(element); + // break; + // case "object": + // content = this._createObjectElement(element); + // break; + // case "object-list": + // content = this._createObjectListElement(element); + // break; + default: + content = {text: element.type}; + // throw new Error("Element type not supported:" + element.type); + } + } + + // Initialize element values + const layout = this.#getDefaultLayout(element, section); + const width = this.#getElementWidth(element, section) || 12; + // + // // Initialize container values + // const elementContainerClassName = element.display?.containerClassName ?? ""; + // const elementContainerStyle = element.display?.containerStyle ?? ""; + // + // // Initialize title values + let title = element.title ?? element.name; // element.name is deprecated --> use element.title + // const titleClassName = element.display?.titleClassName ?? element.display?.labelClasses ?? ""; + // const titleStyle = element.display?.titleStyle ?? element.display?.labelStyle ?? ""; + const titleVisible = element.display?.titleVisible ?? true; + const titleWidth = title && titleVisible ? this.#getElementTitleWidth(element, section) : 0; + // const titleAlign = element.display?.titleAlign ?? element.display?.labelAlign ?? "left"; + // const titleRequiredMark = element.required ? html`*` : ""; + // + // // Help message + // const helpMessage = this._getHelpMessage(element); + // const helpMode = this._getHelpMode(element); + + // Templates are allowed in the names + if (title?.includes("${")) { + title = this.applyTemplate(title); + } + + let result; + // Check for horizontal layout + if (layout === "horizontal") { + // return html` + //
+ // ${title && titleVisible ? html` + //
+ // + //
+ // ` : null} + //
+ //
${content}
+ // ${helpMessage && helpMode === "block" ? html` + //
+ // + //
+ // ` : null} + //
+ //
+ // `; + + result = { + columnGap: this.pdfConfig?.columnGap, + columns: [ + { + text: {text: title, style: "title"}, + // content, + width: (titleWidth * 100 / 12) + "%" + }, + { + ...content, + width: (width * 100 / 12) + "%" + }, + ] + }; + } else { + // layout ===Vertical + // return html` + //
+ //
+ // ${title && titleVisible ? html` + // + // ` : null} + //
${content}
+ //
+ //
+ // `; + + } + return result; + } + + _createComplexElement(element, data = this.data, section) { + // if (!element.display?.template) { + // return this._createElementTemplate(element, null, null, { + // message: "No template provided", + // className: "text-danger" + // }); + // } + + // const content = html` + // + // ${UtilsNew.renderHTML(this.applyTemplate(element.display.template, data, this._getDefaultValue(element, section), element))} + // + // `; + + // return this._createElementTemplate(element, null, content); + return this.applyTemplate(element.display.template, data, this.#getDefaultValue(element, section), element); + } + + _createListElement(element, data = this.data, section) { + // Get values + let values; + if (element.field) { + values = this.#getValue(element.field, data); + } else { + values = element.display.getData(data); + } + const contentLayout = element.display?.contentLayout || "vertical"; + + // 1. Check array and layout exist + if (!Array.isArray(values)) { + // return this._createElementTemplate(element, null, null, { + // message: this._getDefaultValue(element, section) ?? `Field '${element.field}' is not an array`, + // className: "text-danger" + // }); + } + if (contentLayout !== "horizontal" && contentLayout !== "vertical" && contentLayout !== "bullets" && contentLayout !== "numbers") { + // return this._createElementTemplate(element, null, null, { + // message: "Content layout must be 'horizontal', 'vertical' or 'bullets'", + // className: "text-danger" + // }); + } + + // 2. Apply 'filter' and 'transform' functions if defined + if (typeof element.display?.filter === "function") { + values = element.display.filter(values); + } + if (typeof element.display?.transform === "function") { + values = element.display.transform(values); + } + + // 3. Check length of the array. This MUST be done after filtering + if (values.length === 0) { + // If empty we just print the defaultValue, this is not an error + // return this._createElementTemplate(element, null, null, { + // message: this._getDefaultValue(element, section) ?? "Empty array", + // }); + } + + // 4. Format list elements. Initialise values with array, this is valid for scalars, or when 'template' and 'format' do not exist + // Apply the template to all Array elements and store them in 'values' + if (element.display?.format || element.display?.render) { + // NOTE: 'element.display.render' is now deprecated, use 'format' instead + if (element.display?.format) { + values = values.map(item => element.display.format(item, data)); + } else { + values = values.map(item => element.display.render(item, data)); + } + } else { + if (element.display?.template) { + values = values + .map(item => this.applyTemplate(element.display.template, item, this.#getDefaultValue(element, section), element)); + } + } + + // 5. Precompute styles + // const styles = {}; + // if (element.display?.style) { + // if (typeof element.display.style === "string") { + // // All elements will have the same style + // values.forEach(item => styles[item] = element.display.style); + // } else { + // // It is an object, we must find the right style for each element + // for (const item of values) { + // // This call already checks if style is a function + // styles[item] = this._parseStyleField(element.display?.style, item, data); + // } + // } + // } + + // 6. Precompute separators + // const separators = {}; + // if (element.display?.separator) { + // // Last element cannot add a separator, so we iterate until length -1 + // for (let i = 0; i < values.length - 1; i++) { + // let separator = null; + // if (typeof element.display.separator === "string") { + // separator = element.display.separator; + // } else { + // separator = element.display.separator(values[i], i, values, data); + // } + // // if (separator) { + // // separators[values[i]] = separator.includes("---") ? "
" : separator; + // // } + // separators[i] = separator.includes("---") ? "
" : separator; + // } + // } + + // 7. Render element values + let content = {text: this.#getDefaultValue(element, section)}; + switch (contentLayout) { + // case "horizontal": + // content = ` + // ${values + // .map((elem, index) => ` + // ${elem} + // ${index < values.length - 1 ? separators[index] ?? ", " : ""} + // `) + // .join(``) + // }`; + // break; + case "vertical": + // content = ` + // ${values + // .map((elem, index) => ` + //
${elem}
+ // ${separators[index] ? `
${separators[index]}
` : ""} + // `) + // .join("") + // }`; + content = { + stack: values.map((elem, index) => { + return {text: elem}; + }) + }; + break; + case "bullets": + // content = ` + //
    + // ${values + // .map((elem, index) => ` + //
  • ${elem}
  • + // ${separators[index] ? `
    ${separators[index]}
    ` : ""} + // `) + // .join("") + // } + //
+ // `; + content = { + ul: values.map((elem, index) => { + return {text: elem}; + }) + }; + break; + case "numbers": + // content = ` + //
    + // ${values + // .map((elem, index) => ` + //
  1. ${elem}
  2. + // ${separators[index] ? `
    ${separators[index]}
    ` : ""} + // `) + // .join("") + // } + //
+ // `; + content = { + ol: values.map((elem, index) => { + return {text: elem}; + }) + }; + break; + } + + // return this._createElementTemplate(element, null, content); + return content; } #transformData() { @@ -365,8 +931,9 @@ export default class PdfBuilder { return htmlToPdfmake(container, {...defaultHtmlStyle}); } - getDefaultConfig() { + getDefaultPdfConfig() { return { + columnGap: 25, defaultStyle: { fontSize: 10, }, @@ -391,6 +958,11 @@ export default class PdfBuilder { fontSize: 14, bold: true, }, + title: { + // fontSize: 12, + bold: true, + lineHeight: 1.5 + }, body: { fontSize: 12, }, diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index d71f6d7b17..200df1c24e 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -67,6 +67,7 @@ export default class IndividualView extends LitElement { defaultValue: "-", defaultLayout: "horizontal", buttonsVisible: false, + pdf: true }; this._config = this.getDefaultConfig(); } From 78c252a8e6e1b35dfcc836eaf0cfea7c6d829e68 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 24 Jan 2024 17:16:10 +0100 Subject: [PATCH 033/153] wc - Fixed formatter variant function name in variant-interpreter-grid Signed-off-by: gpveronica --- .../variant/interpretation/variant-interpreter-grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 2f30235a51..00bed96ec2 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -608,7 +608,7 @@ export default class VariantInterpreterGrid extends LitElement { field: "id", rowspan: 2, colspan: 1, - formatter: (value, row, index) => VariantGridFormatter.variantFormatter(value, row, index, this.opencgaSession.project.organism.assembly, this._config), + formatter: (value, row, index) => VariantGridFormatter.variantIdFormatter(value, row, index, this.opencgaSession.project.organism.assembly, this._config), halign: this.displayConfigDefault.header.horizontalAlign, // sortable: true visible: this.gridCommons.isColumnVisible("id"), From e6dc7db95db516e0c1eedf6f6502c3598a396f92 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 25 Jan 2024 11:10:19 +0100 Subject: [PATCH 034/153] wc - Removed html tags from text element Signed-off-by: gpveronica --- src/webcomponents/commons/catalog-browser-grid-config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/commons/catalog-browser-grid-config.js b/src/webcomponents/commons/catalog-browser-grid-config.js index 35fff4dd1b..7d11f3af5e 100644 --- a/src/webcomponents/commons/catalog-browser-grid-config.js +++ b/src/webcomponents/commons/catalog-browser-grid-config.js @@ -225,7 +225,7 @@ export default class CatalogBrowserGridConfig extends LitElement { }, { type: "text", - text: "Select the columns to be displayed", + text: "Select the columns to be displayed", display: { containerStyle: "margin: 20px 5px 5px 0px", } From d8e0a0931d7cc681b75366f2d15c495d6ddbe225 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 25 Jan 2024 11:28:51 +0100 Subject: [PATCH 035/153] wc - Fixed object-list and object elements render --- src/webcomponents/commons/forms/data-form.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 49d24f5cd2..12acbce006 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -788,7 +788,10 @@ export default class DataForm extends LitElement { if (typeof content === "string") { contentHtml = UtilsNew.renderHTML(content); } else { - if (Array.isArray(content)) { + // Note 20240125 Vero: + // Added a second condition that ensures that all elements of the array are strings. + // Some of the content arrays are arrays of lit objects (e.g, content received from object-list elements) + if (Array.isArray(content) && content.every(element => typeof element === "string")) { contentHtml = UtilsNew.renderHTML(content.join("")); } } From 334c3db9106169831dd3b00f5210741bf7274a4c Mon Sep 17 00:00:00 2001 From: Josemi Date: Tue, 30 Jan 2024 12:12:13 +0100 Subject: [PATCH 036/153] wc: fix displaying trait items when source is not cosmit or clinvar #TASK-5503 --- src/webcomponents/variant/variant-grid-formatter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/variant/variant-grid-formatter.js b/src/webcomponents/variant/variant-grid-formatter.js index 177c4c0620..197a91f246 100644 --- a/src/webcomponents/variant/variant-grid-formatter.js +++ b/src/webcomponents/variant/variant-grid-formatter.js @@ -1221,7 +1221,7 @@ export default class VariantGridFormatter { clinvar.push({ values: values }); - } else { // COSMIC section + } else if (trait.source.name.toUpperCase() === "COSMIC") { // Prepare data to group by histologySubtype field const key = trait.id + ":" + trait.somaticInformation.primaryHistology + ":" + trait.somaticInformation.primaryHistology; const reviewStatus = trait.additionalProperties.find(p => p.id === "MUTATION_SOMATIC_STATUS"); From ac35e1f713b9479f390eb50f1afdfd9f1b108e88 Mon Sep 17 00:00:00 2001 From: Josemi Date: Tue, 30 Jan 2024 12:13:56 +0100 Subject: [PATCH 037/153] wc: minor code refactor in clinicalTableDetail formatter #TASK-5503 --- src/webcomponents/variant/variant-grid-formatter.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/variant-grid-formatter.js b/src/webcomponents/variant/variant-grid-formatter.js index 197a91f246..b772709c8b 100644 --- a/src/webcomponents/variant/variant-grid-formatter.js +++ b/src/webcomponents/variant/variant-grid-formatter.js @@ -1207,10 +1207,11 @@ export default class VariantGridFormatter { const cosmicIntermediate = new Map(); for (const trait of row.annotation.traitAssociation) { const values = []; + const source = (trait?.source?.name || "").toUpperCase(); const vcvId = trait.additionalProperties.find(p => p.name === "VCV ID"); const genomicFeature = trait.genomicFeatures.find(f => f.featureType.toUpperCase() === "GENE"); const reviewStatus = trait.additionalProperties.find(p => p.name === "ReviewStatus_in_source_file"); - if (trait.source.name.toUpperCase() === "CLINVAR") { + if (source === "CLINVAR") { values.push(`${trait.id}`); values.push(vcvId ? vcvId.value : trait.id); values.push(genomicFeature?.xrefs ? genomicFeature.xrefs?.symbol : "-"); @@ -1221,7 +1222,7 @@ export default class VariantGridFormatter { clinvar.push({ values: values }); - } else if (trait.source.name.toUpperCase() === "COSMIC") { + } else if (source === "COSMIC") { // Prepare data to group by histologySubtype field const key = trait.id + ":" + trait.somaticInformation.primaryHistology + ":" + trait.somaticInformation.primaryHistology; const reviewStatus = trait.additionalProperties.find(p => p.id === "MUTATION_SOMATIC_STATUS"); From 612596e44dd7e5fdaa9a02098c0800ae3f3532d4 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Tue, 30 Jan 2024 15:34:13 +0100 Subject: [PATCH 038/153] wc - Fixed autocomplete disabled test error. Renamed DataForm.ARRAY_FIELD_REGULAR_EXPRESSION --- src/webcomponents/commons/forms/data-form.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 12acbce006..5b3bac1a51 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -1240,7 +1240,6 @@ export default class DataForm extends LitElement { return this._createElementTemplate(element, null, content); } - _createTableElement(element, data = this.data, section) { // Get array values let array; @@ -1935,7 +1934,7 @@ export default class DataForm extends LitElement { _isFieldAutocomplete(field) { // example: phenotypes[].1.description if (field?.includes("[].")) { - const match = field.match(DataForm.re); + const match = field.match(DataForm.ARRAY_FIELD_REGULAR_EXPRESSION); return !!(match && typeof this.dataAutocomplete?.[match?.groups?.arrayFieldName]?.[match?.groups?.index]?.[match?.groups?.field] !== "undefined"); } return false; From fd0e34c24d1c795668290b26a4f1af21bf33a84a Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:39:07 +0100 Subject: [PATCH 039/153] wc: add gene formatter for rearrangement browser #TASK-5401 --- .../variant-interpreter-grid-formatter.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js index df74b20162..f9a58eed28 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js @@ -1092,4 +1092,26 @@ export default class VariantInterpreterGridFormatter { return "-"; } + static rearrangementGeneFormatter(variants, visibleGenesByVariant, opencgaSession) { + const separator = `
`; + return variants + .map((variant, index) => { + let resultHtml = "-"; + const visibleGenes = Array.from(visibleGenesByVariant[variant.id] || []); + + if (visibleGenes.length > 0) { + const genesLins = visibleGenes.map(gene => { + const tooltip = VariantGridFormatter.getGeneTooltip(gene, opencgaSession?.project?.organism?.assembly); + return ` + ${gene} + `; + }); + resultHtml = genesLins.join(" "); + } + + return `
Variant ${index + 1}: ${resultHtml}
`; + }) + .join(separator); + } + } From 6b32e0639b1391d95431afc1ad53f82033afe232 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:44:36 +0100 Subject: [PATCH 040/153] wc: add method to map genes to displayed variants and fix genes column #TASK-5401 --- .../variant-interpreter-rearrangement-grid.js | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 4b15bc83de..bac070b4e4 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -75,6 +75,10 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.review = false; this.variantsReview = null; + // OpenCGA returns the same genes in both variants of the rearrangement + // This map is used to assign the correct genes to each variant + this.genesByVariant = {}; + // Set colors // consequenceTypesImpact; // eslint-disable-next-line no-undef @@ -204,6 +208,52 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { return pairs; } + generateGenesMapFromVariants(variants) { + this.genesByVariant = {}; + const genesList = new Set(); + (variants || []).forEach(variant => { + if (variant?.annotation?.consequenceTypes) { + variant.annotation.consequenceTypes.forEach(ct => { + if (ct.geneName || ct.geneId) { + genesList.add(ct.geneName || ct.geneId); + } + }); + } + }); + + // Request gene info to cellbase + if (genesList.size > 0) { + const genesIds = Array.from(genesList); + return this.opencgaSession.cellbaseClient + .getGeneClient(genesIds.join(","), "info", { + "include": "id,name,chromosome,start,end", + }) + .then(response => { + // 1. Map each gene with it's correct position + const genes = new Map(); + genesIds.forEach((geneId, index) => { + if (response?.responses?.[index]?.results?.[0]) { + genes.set(geneId, response.responses[index].results[0]); + } + }); + + // 2. Assign genes to each variant + variants.forEach(variant => { + const id = variant.id; + this.genesByVariant[id] = new Set(); + (variant?.annotation?.consequenceTypes || []).forEach(ct => { + const gene = genes.get(ct.geneName || ct.geneId); + + // Check if this gene exists and overlaps this variant + if (gene && (variant.chromosome === gene.chromosome && variant.start <= gene.end && gene.start <= variant.end)) { + this.genesByVariant[id].add(ct.geneName || ct.geneId); + } + }); + }); + }); + } + } + renderVariants() { if (this.clinicalVariants && this.clinicalVariants.length > 0) { this.renderLocalVariants(); @@ -250,6 +300,8 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { variantGrid: this, ajax: params => { + let rearrangementResponse = null; + // Make a deep clone object to manipulate the query sent to OpenCGA const internalQuery = JSON.parse(JSON.stringify(this.query)); @@ -272,13 +324,19 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.opencgaSession.opencgaClient.clinical().queryVariant(filters) .then(res => { this.isApproximateCount = res.responses[0].attributes?.approximateCount ?? false; + rearrangementResponse = res; + // Generate map of genes to variants + return this.generateGenesMapFromVariants(res.responses[0].results); + }) + .then(() => { // pairs will have the following format: [[v1, v2], [v3, v4], [v5, v6]]; - const pairs = this.generateRowsFromVariants(res.responses[0].results); + const results = rearrangementResponse.responses[0].results; + const pairs = this.generateRowsFromVariants(results); // It's important to overwrite results array - res.responses[0].results = pairs; + rearrangementResponse.responses[0].results = pairs; - params.success(res); + params.success(rearrangementResponse); }) .catch(e => params.error(e)) .finally(() => { @@ -509,7 +567,9 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { field: "gene", rowspan: 2, colspan: 1, - formatter: (value, row, index) => VariantGridFormatter.geneFormatter(row[0], index, this.query, this.opencgaSession), + formatter: (value, row) => { + return VariantInterpreterGridFormatter.rearrangementGeneFormatter(row, this.genesByVariant, this.opencgaSession); + }, halign: "center", visible: this.gridCommons.isColumnVisible("gene"), }, From b12532f1b265251d621950c53c85bbab131508ab Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:50:42 +0100 Subject: [PATCH 041/153] wc: allow to generate genes map also when rendering local rearrangements #TASK-5401 --- .../variant-interpreter-rearrangement-grid.js | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index bac070b4e4..a102972a2f 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -367,9 +367,29 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.table = $("#" + this.gridId); this.table.bootstrapTable("destroy"); this.table.bootstrapTable({ - data: variants, columns: this._getDefaultColumns(), - sidePagination: "local", + sidePagination: "server", + // Josemi Note 2024-01-31: we have added the ajax function for local variants for getting genes info + // and map the genes to each variant + ajax: params => { + const tableOptions = $(this.table).bootstrapTable("getOptions"); + const limit = params.data.limit || tableOptions.pageSize; + const skip = params.data.offset || 0; + const rows = variants.slice(skip, skip + limit); + + // Generate map of genes to variants + this.generateGenesMapFromVariants(rows) + .then(() => params.success(rows)) + .catch(error => params.error(error)); + }, + // Josemi Note 2024-01-31: we use this method to tell bootstrap-table how many rows we have in our data + responseHandler: response => { + return { + total: variants.length, + rows: response, + }; + }, + iconsPrefix: GridCommons.GRID_ICONS_PREFIX, icons: GridCommons.GRID_ICONS, From 7db508547c866dddacf657e4d9dfac7853ce1824 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:53:01 +0100 Subject: [PATCH 042/153] wc: rename gene feature overlap formatter #TASK-5401 --- .../interpretation/variant-interpreter-grid-formatter.js | 2 +- .../interpretation/variant-interpreter-rearrangement-grid.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js index f9a58eed28..6393048ca3 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js @@ -1017,7 +1017,7 @@ export default class VariantInterpreterGridFormatter { `; } - static geneFeatureOverlapFormatter(variant, opencgaSession) { + static rearrangementFeatureOverlapFormatter(variant, opencgaSession) { if (variant?.annotation?.consequenceTypes) { const overlaps = []; (variant.annotation.consequenceTypes || []).forEach(ct => { diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index a102972a2f..ad8a690d3c 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -687,7 +687,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { colspan: 1, rowspan: 1, formatter: (value, rows) => { - return VariantInterpreterGridFormatter.geneFeatureOverlapFormatter(rows[0], this.opencgaSession); + return VariantInterpreterGridFormatter.rearrangementFeatureOverlapFormatter(rows[0], this.opencgaSession); }, halign: "center", valign: "top", @@ -699,7 +699,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { colspan: 1, rowspan: 1, formatter: (value, rows) => { - return VariantInterpreterGridFormatter.geneFeatureOverlapFormatter(rows[1], this.opencgaSession); + return VariantInterpreterGridFormatter.rearrangementFeatureOverlapFormatter(rows[1], this.opencgaSession); }, halign: "center", valign: "top", From f6e34719ed432b439d574646b139f8fc5caab063 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:55:59 +0100 Subject: [PATCH 043/153] wc: filter feature overlaps by genes of this variant #TASK-5401 --- .../variant-interpreter-grid-formatter.js | 50 ++++++++++--------- .../variant-interpreter-rearrangement-grid.js | 4 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js index 6393048ca3..b100272465 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js @@ -1017,36 +1017,38 @@ export default class VariantInterpreterGridFormatter { `; } - static rearrangementFeatureOverlapFormatter(variant, opencgaSession) { + static rearrangementFeatureOverlapFormatter(variant, genes, opencgaSession) { if (variant?.annotation?.consequenceTypes) { const overlaps = []; - (variant.annotation.consequenceTypes || []).forEach(ct => { - if (Array.isArray(ct.exonOverlap) && ct.exonOverlap?.length > 0) { - ct.exonOverlap.map(exon => { - overlaps.push({ - geneName: ct.geneName || "", - transcript: ct.transcript || ct.ensemblTranscriptId || "", - feature: `exon (${exon.number || "-"})`, - }); - }); - } else if (Array.isArray(ct.sequenceOntologyTerms) && ct.sequenceOntologyTerms?.length > 0) { - ct.sequenceOntologyTerms.forEach(term => { - if (term.name === "intron_variant") { - overlaps.push({ - geneName: ct.geneName || "", - transcript: ct.transcript || ct.ensemblTranscriptId || "", - feature: "intron", - }); - } else if (term.name === "5_prime_UTR_variant" || term.name === "3_prime_UTR_variant") { + (variant.annotation.consequenceTypes || []) + .filter(ct => genes.has(ct.geneName || ct.geneId || "")) + .forEach(ct => { + if (Array.isArray(ct.exonOverlap) && ct.exonOverlap?.length > 0) { + ct.exonOverlap.map(exon => { overlaps.push({ geneName: ct.geneName || "", transcript: ct.transcript || ct.ensemblTranscriptId || "", - feature: `${term.name.charAt(0)}'-UTR`, + feature: `exon (${exon.number || "-"})`, }); - } - }); - } - }); + }); + } else if (Array.isArray(ct.sequenceOntologyTerms) && ct.sequenceOntologyTerms?.length > 0) { + ct.sequenceOntologyTerms.forEach(term => { + if (term.name === "intron_variant") { + overlaps.push({ + geneName: ct.geneName || "", + transcript: ct.transcript || ct.ensemblTranscriptId || "", + feature: "intron", + }); + } else if (term.name === "5_prime_UTR_variant" || term.name === "3_prime_UTR_variant") { + overlaps.push({ + geneName: ct.geneName || "", + transcript: ct.transcript || ct.ensemblTranscriptId || "", + feature: `${term.name.charAt(0)}'-UTR`, + }); + } + }); + } + }); if (overlaps.length > 0) { const maxDisplayedOverlaps = 3; diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index ad8a690d3c..59f44eebae 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -687,7 +687,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { colspan: 1, rowspan: 1, formatter: (value, rows) => { - return VariantInterpreterGridFormatter.rearrangementFeatureOverlapFormatter(rows[0], this.opencgaSession); + return VariantInterpreterGridFormatter.rearrangementFeatureOverlapFormatter(rows[0], this.genesByVariant[rows[0].id], this.opencgaSession); }, halign: "center", valign: "top", @@ -699,7 +699,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { colspan: 1, rowspan: 1, formatter: (value, rows) => { - return VariantInterpreterGridFormatter.rearrangementFeatureOverlapFormatter(rows[1], this.opencgaSession); + return VariantInterpreterGridFormatter.rearrangementFeatureOverlapFormatter(rows[1], this.genesByVariant[rows[1].id], this.opencgaSession); }, halign: "center", valign: "top", From c2b0818fb63ba556201b5f7109988bd7c63baff5 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:57:08 +0100 Subject: [PATCH 044/153] wc: fix typo in rearrangement gene formatter #TASK-5401 --- .../variant-interpreter-grid-formatter.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js index b100272465..1a7a3ccd0d 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js @@ -1094,21 +1094,21 @@ export default class VariantInterpreterGridFormatter { return "-"; } - static rearrangementGeneFormatter(variants, visibleGenesByVariant, opencgaSession) { + static rearrangementGeneFormatter(variants, genesByVariant, opencgaSession) { const separator = `
`; return variants .map((variant, index) => { let resultHtml = "-"; - const visibleGenes = Array.from(visibleGenesByVariant[variant.id] || []); + const genes = Array.from(genesByVariant[variant.id] || []); - if (visibleGenes.length > 0) { - const genesLins = visibleGenes.map(gene => { + if (genes.length > 0) { + const genesLinks = genes.map(gene => { const tooltip = VariantGridFormatter.getGeneTooltip(gene, opencgaSession?.project?.organism?.assembly); return ` ${gene} `; }); - resultHtml = genesLins.join(" "); + resultHtml = genesLinks.join(" "); } return `
Variant ${index + 1}: ${resultHtml}
`; From 060035be62b7855f23249397f0f5a969557920ab Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 17:59:17 +0100 Subject: [PATCH 045/153] wc: add offset when generating the list of genes by variant #TASK-5401 --- .../variant-interpreter-rearrangement-grid.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 59f44eebae..5c7820bbed 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -208,7 +208,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { return pairs; } - generateGenesMapFromVariants(variants) { + generateGenesMapFromVariants(variants, offset = 5000) { this.genesByVariant = {}; const genesList = new Set(); (variants || []).forEach(variant => { @@ -245,7 +245,11 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { const gene = genes.get(ct.geneName || ct.geneId); // Check if this gene exists and overlaps this variant - if (gene && (variant.chromosome === gene.chromosome && variant.start <= gene.end && gene.start <= variant.end)) { + const start = gene.start - offset; + const end = gene.end + offset; + const variantStart = Math.min(variant.start, variant.end); + const variantEnd = Math.max(variant.start, variant.end); + if (gene && (variant.chromosome === gene.chromosome && variantStart <= end && start <= variantEnd)) { this.genesByVariant[id].add(ct.geneName || ct.geneId); } }); From 20153e586ff58f9f52c782d1479746aa4b46451c Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 18:02:45 +0100 Subject: [PATCH 046/153] wc: fix displaying gene in feature orverlap formatter #TASK-5401 --- .../interpretation/variant-interpreter-grid-formatter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js index 1a7a3ccd0d..d29fd3201b 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid-formatter.js @@ -1026,7 +1026,7 @@ export default class VariantInterpreterGridFormatter { if (Array.isArray(ct.exonOverlap) && ct.exonOverlap?.length > 0) { ct.exonOverlap.map(exon => { overlaps.push({ - geneName: ct.geneName || "", + geneName: ct.geneName || ct.geneId || "", transcript: ct.transcript || ct.ensemblTranscriptId || "", feature: `exon (${exon.number || "-"})`, }); @@ -1035,13 +1035,13 @@ export default class VariantInterpreterGridFormatter { ct.sequenceOntologyTerms.forEach(term => { if (term.name === "intron_variant") { overlaps.push({ - geneName: ct.geneName || "", + geneName: ct.geneName || ct.geneId || "", transcript: ct.transcript || ct.ensemblTranscriptId || "", feature: "intron", }); } else if (term.name === "5_prime_UTR_variant" || term.name === "3_prime_UTR_variant") { overlaps.push({ - geneName: ct.geneName || "", + geneName: ct.geneName || ct.geneId || "", transcript: ct.transcript || ct.ensemblTranscriptId || "", feature: `${term.name.charAt(0)}'-UTR`, }); From f53f1af3513b966aa65bd2e59b2debb7664ec94a Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 31 Jan 2024 19:07:59 +0100 Subject: [PATCH 047/153] wc: fix checking gene overlap with rearrangement #TASK-5401 --- .../variant-interpreter-rearrangement-grid.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 5c7820bbed..4f5f3e0d86 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -245,12 +245,14 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { const gene = genes.get(ct.geneName || ct.geneId); // Check if this gene exists and overlaps this variant - const start = gene.start - offset; - const end = gene.end + offset; - const variantStart = Math.min(variant.start, variant.end); - const variantEnd = Math.max(variant.start, variant.end); - if (gene && (variant.chromosome === gene.chromosome && variantStart <= end && start <= variantEnd)) { - this.genesByVariant[id].add(ct.geneName || ct.geneId); + if (gene) { + const start = gene.start - offset; + const end = gene.end + offset; + const variantStart = Math.min(variant.start, variant.end); + const variantEnd = Math.max(variant.start, variant.end); + if (variant.chromosome === gene.chromosome && variantStart <= end && start <= variantEnd) { + this.genesByVariant[id].add(ct.geneName || ct.geneId); + } } }); }); @@ -342,7 +344,10 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { params.success(rearrangementResponse); }) - .catch(e => params.error(e)) + .catch(error => { + console.error(error); + params.error(error); + }) .finally(() => { LitUtils.dispatchCustomEvent(this, "queryComplete", null); }); From 9836598fe050d0ec6342dad92c537cb6a58e719d Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 1 Feb 2024 16:30:28 +0100 Subject: [PATCH 048/153] wc: exclude some keys not allowed in export endpoint #TASK-5525 --- src/webcomponents/commons/opencga-export.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/webcomponents/commons/opencga-export.js b/src/webcomponents/commons/opencga-export.js index 832357d52f..14bcdcc199 100644 --- a/src/webcomponents/commons/opencga-export.js +++ b/src/webcomponents/commons/opencga-export.js @@ -88,6 +88,17 @@ export default class OpencgaExport extends LitElement { "json": "JSON", }; + this.excludedVariantQueryFields = [ + // Fields not supported in variant export + "count", + "approximateCount", + "approximateCountSamplingSize", + "includeInterpretation", + // Fields used for the pagination and does not apply on export + "limit", + "skip", + ]; + this.mode = "sync"; this.format = "tab"; this.query = {}; @@ -359,6 +370,12 @@ const client = new OpenCGAClient({ outputFileName: "variants", outputFileFormat: this.outputFileFormats[this.format], }; + + // Exclude keys from the query object that are not supported in export endpoint + this.excludedVariantQueryFields.forEach(key => { + delete data[key]; + }); + const params = { study: this.opencgaSession.study.fqn, }; From 8c586ad47a33df56dbf7fd63e59e2812c3aa054d Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 1 Feb 2024 16:36:17 +0100 Subject: [PATCH 049/153] wc: include selected samples for the proband also in the family members object when creating a new family case #TASK-5559 --- .../clinical/clinical-analysis-create.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/clinical/clinical-analysis-create.js b/src/webcomponents/clinical/clinical-analysis-create.js index 13ffc0b0d7..0043c2f764 100644 --- a/src/webcomponents/clinical/clinical-analysis-create.js +++ b/src/webcomponents/clinical/clinical-analysis-create.js @@ -324,9 +324,21 @@ export default class ClinicalAnalysisCreate extends LitElement { data.family = { id: this.clinicalAnalysis.family.id, members: this.clinicalAnalysis.family.members.map(member => { - return { + const familyMember = { id: member.id, }; + + // We need to include the selected samples for the member of the family that is also + // the proband in the clinical analysis + if (member.id === this.clinicalAnalysis.proband.id) { + familyMember.samples = this.clinicalAnalysis.samples.map(sample => { + return { + id: sample.id, + }; + }); + } + + return familyMember; }), }; } From 62594450c5b579aa0fd1947a5dd8d72fd9d97414 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 2 Feb 2024 12:06:44 +0100 Subject: [PATCH 050/153] wc: allow to hide CADD and SpliceAi columns in vairant interpreter grid #TASK-5542 --- .../variant/interpretation/variant-interpreter-grid.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 2f30235a51..0c33721003 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -888,7 +888,9 @@ export default class VariantInterpreterGrid extends LitElement { formatter: (value, row) => VariantGridFormatter.caddScaledFormatter(value, row), align: "right", halign: this.displayConfigDefault.header.horizontalAlign, - visible: this.gridCommons.isColumnVisible("cadd", "deleteriousness"), + visible: !this._config.hideDeleteriousnessCADD && this.gridCommons.isColumnVisible("cadd", "deleteriousness"), + excludeFromSettings: this._config.hideDeleteriousnessCADD, + excludeFromExport: this._config.hideDeleteriousnessCADD, }, { id: "spliceai", @@ -899,7 +901,9 @@ export default class VariantInterpreterGrid extends LitElement { formatter: (value, row) => VariantGridFormatter.spliceAIFormatter(value, row), align: "right", halign: this.displayConfigDefault.header.horizontalAlign, - visible: this.gridCommons.isColumnVisible("spliceai", "deleteriousness"), + visible: !this._config.hideDeleteriousnessSpliceAi && this.gridCommons.isColumnVisible("spliceai", "deleteriousness"), + excludeFromSettings: this._config.hideDeleteriousnessSpliceAi, + excludeFromExport: this._config.hideDeleteriousnessSpliceAi, }, ...vcfDataColumns, { @@ -1563,6 +1567,8 @@ export default class VariantInterpreterGrid extends LitElement { hideType: false, hidePopulationFrequencies: false, hideClinicalInfo: false, + hideDeleteriousnessCADD: false, + hideDeleteriousnessSpliceAi: false, quality: { qual: 30, From 7a4af316cf5ab0e859f1f75b37a35a0708be1ae0 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 2 Feb 2024 12:07:12 +0100 Subject: [PATCH 051/153] wc: hide cadd and spliceai columns in variant interpreter cnv browser #TASK-5542 --- .../variant/interpretation/variant-interpreter-browser-cnv.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js index d77eac3ed9..aee33e60db 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js @@ -428,6 +428,8 @@ class VariantInterpreterBrowserCNV extends LitElement { hideType: true, hidePopulationFrequencies: true, hideClinicalInfo: true, + hideDeleteriousnessCADD: true, + hideDeleteriousnessSpliceAi: true, genotype: { type: "VAF" From 593a627627a59a0f82131aebdd38b6919939798c Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:03:15 +0100 Subject: [PATCH 052/153] wc: fix actions column to be always visible in rearrangement grid #TASK-5575 --- .../interpretation/variant-interpreter-rearrangement-grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 4b15bc83de..5081717898 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -596,8 +596,8 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { events: { "click a": this.onActionClick.bind(this) }, - visible: this._config.showActions && !this._config?.columns?.hidden?.includes("actions"), excludeFromExport: true, + excludeFromSettings: true, } ], [ From d2e5bca01ddb40d5f70b0c150195b63a86a82cbd Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:04:35 +0100 Subject: [PATCH 053/153] wc: prevent changing disabled attribute in edit button of actions dropdown when actions column is not visible #TASK-5575 --- .../variant-interpreter-rearrangement-grid.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 5081717898..dd1c227f70 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -846,10 +846,12 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { // Set 'Edit' button as enabled/disabled document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewButton`).disabled = !event.currentTarget.checked; const reviewActionButton = document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewActionButton`); - if (event.currentTarget.checked) { - reviewActionButton.removeAttribute("disabled"); - } else { - reviewActionButton.setAttribute("disabled", "true"); + if (reviewActionButton) { + if (event.currentTarget.checked) { + reviewActionButton.removeAttribute("disabled"); + } else { + reviewActionButton.setAttribute("disabled", "true"); + } } // Dispatch row check event From 0c7f7fbd3a7b81995945a71061663a5df3310590 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:07:04 +0100 Subject: [PATCH 054/153] wc: apply the same fix in variant-interpreter-grid #TASK-5575 --- .../interpretation/variant-interpreter-grid.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 2f30235a51..75662f9b45 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -1307,13 +1307,18 @@ export default class VariantInterpreterGrid extends LitElement { this.checkedVariants.delete(variantId); } - // Set 'Edit' button as enabled/disabled - document.getElementById(`${this._prefix}${variantId}VariantReviewButton`).disabled = !e.currentTarget.checked; + // Set 'Edit' button as enabled/disabled in column and actions dropdown + const reviewButton = document.getElementById(`${this._prefix}${variantId}VariantReviewButton`); + if (reviewButton) { + reviewButton.disabled = !e.currentTarget.checked; + } const reviewActionButton = document.getElementById(`${this._prefix}${variantId}VariantReviewActionButton`); - if (e.currentTarget.checked) { - reviewActionButton.removeAttribute("disabled"); - } else { - reviewActionButton.setAttribute("disabled", "true"); + if (reviewActionButton) { + if (e.currentTarget.checked) { + reviewActionButton.removeAttribute("disabled"); + } else { + reviewActionButton.setAttribute("disabled", "true"); + } } // Enable or disable evidences select From d3252fbe5dd0942e556ebb63c4213599b17d068b Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:12:54 +0100 Subject: [PATCH 055/153] wc: added note in variant-interpreter-grid #TAKS-5575 --- .../variant/interpretation/variant-interpreter-grid.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 75662f9b45..fa9366757e 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -1308,10 +1308,10 @@ export default class VariantInterpreterGrid extends LitElement { } // Set 'Edit' button as enabled/disabled in column and actions dropdown - const reviewButton = document.getElementById(`${this._prefix}${variantId}VariantReviewButton`); - if (reviewButton) { - reviewButton.disabled = !e.currentTarget.checked; - } + document.getElementById(`${this._prefix}${variantId}VariantReviewButton`).disabled = !e.currentTarget.checked; + + // Josemi NOTE 20240205 - Edit buton in actions dropdown is not rendered when when actions column is hidden + // We have added a condition to ensure that the button exists before set/remove the disabled attribute const reviewActionButton = document.getElementById(`${this._prefix}${variantId}VariantReviewActionButton`); if (reviewActionButton) { if (e.currentTarget.checked) { From 576becbcbc83ccf8c0786f81db925ad3e559ac5a Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:13:10 +0100 Subject: [PATCH 056/153] wc: added note about last change in rearrangement grid #TAKS-5575 --- .../interpretation/variant-interpreter-rearrangement-grid.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index dd1c227f70..4fa0c021a6 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -845,6 +845,9 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { // Set 'Edit' button as enabled/disabled document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewButton`).disabled = !event.currentTarget.checked; + + // Josemi NOTE 20240205 - Edit buton in actions dropdown is not rendered when when actions column is hidden + // We have added a condition to ensure that the button exists before set/remove the disabled attribute const reviewActionButton = document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewActionButton`); if (reviewActionButton) { if (event.currentTarget.checked) { From 215ff07b2df60a461275abcd974780a08432f438 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:17:12 +0100 Subject: [PATCH 057/153] wc: fix note and add check also to edit button in review column #TASK-5575 --- .../variant/interpretation/variant-interpreter-grid.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index fa9366757e..867216c757 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -1307,11 +1307,15 @@ export default class VariantInterpreterGrid extends LitElement { this.checkedVariants.delete(variantId); } - // Set 'Edit' button as enabled/disabled in column and actions dropdown - document.getElementById(`${this._prefix}${variantId}VariantReviewButton`).disabled = !e.currentTarget.checked; + // Set 'Edit' button as enabled/disabled in 'Review' column + // Josemi NOTE 20240205 - Edit buton in column is not rendered when 'Review' column is hidden + const reviewButton = document.getElementById(`${this._prefix}${variantId}VariantReviewButton`); + if (reviewButton) { + reviewButton.disabled = !e.currentTarget.checked; + } + // Set 'Edit' button as enabled/disabled in 'Actions' dropdown // Josemi NOTE 20240205 - Edit buton in actions dropdown is not rendered when when actions column is hidden - // We have added a condition to ensure that the button exists before set/remove the disabled attribute const reviewActionButton = document.getElementById(`${this._prefix}${variantId}VariantReviewActionButton`); if (reviewActionButton) { if (e.currentTarget.checked) { From 6945577e29e38e05e31173970bc08fe0badb0315 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 5 Feb 2024 16:17:38 +0100 Subject: [PATCH 058/153] wc: fix note and add check also in edit button of Review column of rearrangement grid #TASK-5575 --- .../variant-interpreter-rearrangement-grid.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 4fa0c021a6..038dfd5568 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -843,11 +843,15 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { } }); - // Set 'Edit' button as enabled/disabled - document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewButton`).disabled = !event.currentTarget.checked; + // Set 'Edit' button as enabled/disabled in 'Review' column + // Josemi NOTE 20240205 - Edit buton in column is not rendered when 'Review' column is hidden + const reviewButton = document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewButton`); + if (reviewButton) { + reviewButton.disabled = !event.currentTarget.checked; + } + // Set 'Edit' button as enabled/disabled in 'Actions' dropdown // Josemi NOTE 20240205 - Edit buton in actions dropdown is not rendered when when actions column is hidden - // We have added a condition to ensure that the button exists before set/remove the disabled attribute const reviewActionButton = document.getElementById(`${this._prefix}${this._rows[index][0].id}VariantReviewActionButton`); if (reviewActionButton) { if (event.currentTarget.checked) { From c57278bfe2a67ad9e5abb0b02eeadc81dcfc70a4 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 7 Feb 2024 11:01:52 +0100 Subject: [PATCH 059/153] Increment version to v2.12.3-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddde03524e..125cf44e0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsorolla", - "version": "2.12.2", + "version": "2.12.3-dev", "description": "JSorolla is a JavaScript bioinformatic library for data analysis and genomic visualization", "repository": { "type": "git", From 2055319bcdd66fd80ea25ce4fc0eb6e67cbdaf40 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 7 Feb 2024 11:24:38 +0100 Subject: [PATCH 060/153] wc: simplify configuration parameter to hide deleteriousness column in variant interpreter grid #TASK-5542 --- .../interpretation/variant-interpreter-grid.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 0c33721003..61d454e60f 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -888,9 +888,9 @@ export default class VariantInterpreterGrid extends LitElement { formatter: (value, row) => VariantGridFormatter.caddScaledFormatter(value, row), align: "right", halign: this.displayConfigDefault.header.horizontalAlign, - visible: !this._config.hideDeleteriousnessCADD && this.gridCommons.isColumnVisible("cadd", "deleteriousness"), - excludeFromSettings: this._config.hideDeleteriousnessCADD, - excludeFromExport: this._config.hideDeleteriousnessCADD, + visible: !this._config.hideDeleteriousness && this.gridCommons.isColumnVisible("cadd", "deleteriousness"), + excludeFromSettings: this._config.hideDeleteriousness, + excludeFromExport: this._config.hideDeleteriousness, }, { id: "spliceai", @@ -901,9 +901,9 @@ export default class VariantInterpreterGrid extends LitElement { formatter: (value, row) => VariantGridFormatter.spliceAIFormatter(value, row), align: "right", halign: this.displayConfigDefault.header.horizontalAlign, - visible: !this._config.hideDeleteriousnessSpliceAi && this.gridCommons.isColumnVisible("spliceai", "deleteriousness"), - excludeFromSettings: this._config.hideDeleteriousnessSpliceAi, - excludeFromExport: this._config.hideDeleteriousnessSpliceAi, + visible: !this._config.hideDeleteriousness && this.gridCommons.isColumnVisible("spliceai", "deleteriousness"), + excludeFromSettings: this._config.hideDeleteriousness, + excludeFromExport: this._config.hideDeleteriousness, }, ...vcfDataColumns, { @@ -1567,8 +1567,7 @@ export default class VariantInterpreterGrid extends LitElement { hideType: false, hidePopulationFrequencies: false, hideClinicalInfo: false, - hideDeleteriousnessCADD: false, - hideDeleteriousnessSpliceAi: false, + hideDeleteriousness: false, quality: { qual: 30, From 9939c4858431aeb57619c685ebcf442337f7811d Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 7 Feb 2024 11:25:31 +0100 Subject: [PATCH 061/153] wc: fix configuration to hide deleteriousness column in cnv interpreter browser #TASK-5542 --- .../variant/interpretation/variant-interpreter-browser-cnv.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js index aee33e60db..5a63c2c51e 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js @@ -428,8 +428,7 @@ class VariantInterpreterBrowserCNV extends LitElement { hideType: true, hidePopulationFrequencies: true, hideClinicalInfo: true, - hideDeleteriousnessCADD: true, - hideDeleteriousnessSpliceAi: true, + hideDeleteriousness: true, genotype: { type: "VAF" From 0db4c51157fafd0004b0c886e743671ce6c17e1b Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 17:27:50 +0100 Subject: [PATCH 062/153] wc - Tiny style fix Signed-off-by: gpveronica --- cypress/e2e/iva/individual-browser-grid.cy.js | 2 +- src/webcomponents/individual/individual-create.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/iva/individual-browser-grid.cy.js b/cypress/e2e/iva/individual-browser-grid.cy.js index ff21ff3cf1..000ce030c9 100644 --- a/cypress/e2e/iva/individual-browser-grid.cy.js +++ b/cypress/e2e/iva/individual-browser-grid.cy.js @@ -145,7 +145,7 @@ context("Individual Browser Grid", () => { .find("span.select2-selection__rendered") .should("contain.text", "Glioblastoma multiforme"); cy.get("@modal-create") - .find(`input[placeholder="Add phenotype ID......"]`) + .find(`input[placeholder="Add phenotype ID..."]`) .then(element => { expect(element.val()).equal("HP:0012174"); cy.wrap(element).should("be.disabled"); diff --git a/src/webcomponents/individual/individual-create.js b/src/webcomponents/individual/individual-create.js index 7b0e7122d6..bb2858fce7 100644 --- a/src/webcomponents/individual/individual-create.js +++ b/src/webcomponents/individual/individual-create.js @@ -421,7 +421,7 @@ export default class IndividualCreate extends LitElement { field: "phenotypes[].id", type: "input-text", display: { - placeholder: "Add phenotype ID......", + placeholder: "Add phenotype ID...", } }, { From 0e2e1120d845c8e0b14aa966e3bda31dfb64cd75 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 17:28:39 +0100 Subject: [PATCH 063/153] wc - Minor fix, correct test title Signed-off-by: gpveronica --- cypress/e2e/iva/custom-page.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/iva/custom-page.cy.js b/cypress/e2e/iva/custom-page.cy.js index 399e60ea47..96c5c8b61e 100644 --- a/cypress/e2e/iva/custom-page.cy.js +++ b/cypress/e2e/iva/custom-page.cy.js @@ -16,7 +16,7 @@ import UtilsTest from "../../support/utils-test.js"; -context("Sample Browser Grid", () => { +context("Custom page", () => { beforeEach(() => { cy.visit("#aboutzetta"); From 8d4941dae9a8db3f769d12a0d1c8546cefc6522f Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 18:38:26 +0100 Subject: [PATCH 064/153] wc - Added data form table test in a separate component. Tests pending Signed-off-by: gpveronica --- .../webcomponents/data-form-table-test.js | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/sites/test-app/webcomponents/data-form-table-test.js diff --git a/src/sites/test-app/webcomponents/data-form-table-test.js b/src/sites/test-app/webcomponents/data-form-table-test.js new file mode 100644 index 0000000000..f7e25644c7 --- /dev/null +++ b/src/sites/test-app/webcomponents/data-form-table-test.js @@ -0,0 +1,238 @@ + +/** + * Copyright 2015-2023 OpenCB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {html, LitElement} from "lit"; + + +import "../../../webcomponents/commons/forms/data-form.js"; +import VariantTableFormatter from "../../../webcomponents/variant/variant-table-formatter"; +import UtilsNew from "../../../core/utils-new"; + + +class DataFormTableTest extends LitElement { + + constructor() { + super(); + this.#init(); + } + + createRenderRoot() { + return this; + } + + static get properties() { + return { + opencgaSession: { + type: Object, + }, + testVariantFile: { + type: String, + }, + testDataVersion: { + type: String, + }, + config: { + type: Object, + } + }; + } + + #init() { + this._dataFormConfig = this.getDefaultConfig(); + this.variants = []; + this.gridTypes = { + snv: "variantInterpreterCancerSNV", + cnv: "variantInterpreterCancerCNV", + rearrangements: "variantInterpreterRearrangement", + }; + } + + #setLoading(value) { + this.isLoading = value; + this.requestUpdate(); + } + + update(changedProperties) { + if (changedProperties.has("testVariantFile") && + changedProperties.has("testDataVersion") && + changedProperties.has("opencgaSession")) { + this.opencgaSessionObserver(); + } + super.update(changedProperties); + } + + opencgaSessionObserver() { + this.#setLoading(true); + UtilsNew.importJSONFile(`./test-data/${this.testDataVersion}/${this.testVariantFile}.json`) + .then(content => { + this.variants = content; + if (this.testVariantFile === "variant-browser-germline") { + this.germlineMutate(); + } else { + // this.cancerMutate(); + } + }) + .catch(err => { + // this.variants = []; + console.log(err); + }) + .finally(() => { + this.#setLoading(false); + }); + } + + germlineMutate() { + // 1. no gene names in the CT array + this.variants[10].annotation.consequenceTypes.forEach(ct => ct.geneName = null); + + // 2. SIFT with no description available + // this.variants[10].annotation.consequenceTypes + // .filter(ct => ct.proteinVariantAnnotation) + // .forEach(ct => delete ct.proteinVariantAnnotation.substitutionScores[0].description); + // Finally, we update variants mem address to force a rendering + this.variants = [...this.variants]; + } + + onSubmit(e) { + console.log("Data test", this.variants); + console.log("Data form Data", e); + console.log("test input", e); + } + + render() { + return html` + + + `; + } + + getDefaultConfig() { + const gridConfig = { + ...(this.opencgaSession?.user?.configs?.IVA?.settings?.[this.gridTypes.rearrangements]?.grid || {}), + somatic: false, + geneSet: { + ensembl: true, + refseq: false, + }, + consequenceType: { + maneTranscript: true, + gencodeBasicTranscript: true, + ensemblCanonicalTranscript: true, + refseqTranscript: true, + ccdsTranscript: false, + ensemblTslTranscript: false, + proteinCodingTranscript: false, + highImpactConsequenceTypeTranscript: false, + + showNegativeConsequenceTypes: true + }, + populationFrequenciesConfig: { + displayMode: "FREQUENCY_BOX" + }, + variantTypes: ["SNV"], + }; + + return { + title: "Summary", + icon: "", + display: { + collapsable: true, + titleVisible: false, + titleWidth: 2, + defaultValue: "-", + defaultLayout: "horizontal", + buttonsVisible: false, + }, + sections: [ + { + title: "Variants", + display: { + }, + elements: [ + { + title: "List of Variants", + field: "variants", + type: "table", + display: { + defaultLayout: "vertical", + className: "", + style: "", + headerClassName: "", + headerStyle: "", + headerVisible: true, + // filter: array => array.filter(item => item.somatic), + // transform: array => array.map(item => { + // item.somatic = true; + // return item; + // }), + defaultValue: "-", + columns: [ + VariantTableFormatter.variantIdFormatter(), + VariantTableFormatter.geneFormatter(), + VariantTableFormatter.hgvsFormatter(gridConfig), + VariantTableFormatter.typeFormatter(), + VariantTableFormatter.consequenceTypeFormatter("", gridConfig), + { + title: "Deleteriousness", + display: { + columns: [ + VariantTableFormatter.siftFormatter(), + VariantTableFormatter.polyphenFormatter(), + VariantTableFormatter.revelFormatter(), + VariantTableFormatter.caddScaledFormatter(), + VariantTableFormatter.spliceAIFormatter(), + ] + } + }, + { + title: "Conservation", + display: { + columns: [ + VariantTableFormatter.conservationFormatter("PhyloP"), + VariantTableFormatter.conservationFormatter("PhastCons"), + VariantTableFormatter.conservationFormatter("GERP"), + ] + } + }, + // VariantTableFormatter.populationFrequencyFormatter(), + { + title: "Clinical", + display: { + columns: [ + VariantTableFormatter.clinvarFormatter(), + VariantTableFormatter.cosmicFormatter(), + ] + } + } + ], + }, + }, + ], + }, + ], + }; + } + + +} + +customElements.define("data-form-table-test", DataFormTableTest); From f4bb273543067a51428b662248d3ff4db56b4621 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 18:39:09 +0100 Subject: [PATCH 065/153] wc - Added data-cy attribute for test convenience Signed-off-by: gpveronica --- src/webcomponents/variant/variant-browser-grid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/variant/variant-browser-grid.js b/src/webcomponents/variant/variant-browser-grid.js index 71a492cb0b..2b4e77462f 100644 --- a/src/webcomponents/variant/variant-browser-grid.js +++ b/src/webcomponents/variant/variant-browser-grid.js @@ -1052,7 +1052,7 @@ export default class VariantBrowserGrid extends LitElement { ` : null} -
+
From 0a2799c699893a4a64369770de061e33d4f2bff5 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 18:40:03 +0100 Subject: [PATCH 066/153] wc - Removed data form table component from render Signed-off-by: gpveronica --- .../variant-browser-grid-test.js | 142 ++---------------- 1 file changed, 14 insertions(+), 128 deletions(-) diff --git a/src/sites/test-app/webcomponents/variant-browser-grid-test.js b/src/sites/test-app/webcomponents/variant-browser-grid-test.js index c5ac78a7e2..60d80ca261 100644 --- a/src/sites/test-app/webcomponents/variant-browser-grid-test.js +++ b/src/sites/test-app/webcomponents/variant-browser-grid-test.js @@ -23,10 +23,6 @@ import UtilsNew from "../../../core/utils-new.js"; import "../../../webcomponents/commons/forms/data-form.js"; import "../../../webcomponents/loading-spinner.js"; import "../../../webcomponents/variant/variant-browser-grid.js"; -import Types from "../../../webcomponents/commons/types"; -import CatalogGridFormatter from "../../../webcomponents/commons/catalog-grid-formatter"; -import VariantTableFormatter from "../../../webcomponents/variant/variant-table-formatter"; - class VariantBrowserGridTest extends LitElement { @@ -84,6 +80,7 @@ class VariantBrowserGridTest extends LitElement { UtilsNew.importJSONFile(`./test-data/${this.testDataVersion}/${this.testVariantFile}.json`) .then(content => { this.variants = content; + // Fixme 20240107: no distinction between germline and cancer if (this.testVariantFile === "variant-browser-germline") { this.germlineMutate(); } else { @@ -131,133 +128,22 @@ class VariantBrowserGridTest extends LitElement { } return html` -

- Variant Browser (${this.testVariantFile?.split("-")?.at(-1)}) -

- - - - - - +
+

+ Variant Browser (${this.testVariantFile?.split("-")?.at(-1)}) +

+ + +
`; } - getDefaultConfig() { - const gridConfig = { - ...(this.opencgaSession?.user?.configs?.IVA?.settings?.[this.gridTypes.rearrangements]?.grid || {}), - somatic: false, - geneSet: { - ensembl: true, - refseq: false, - }, - consequenceType: { - maneTranscript: true, - gencodeBasicTranscript: true, - ensemblCanonicalTranscript: true, - refseqTranscript: true, - ccdsTranscript: false, - ensemblTslTranscript: false, - proteinCodingTranscript: false, - highImpactConsequenceTypeTranscript: false, - - showNegativeConsequenceTypes: true - }, - populationFrequenciesConfig: { - displayMode: "FREQUENCY_BOX" - }, - variantTypes: ["SNV"], - }; - - return { - title: "Summary", - icon: "", - display: { - collapsable: true, - titleVisible: false, - titleWidth: 2, - defaultValue: "-", - defaultLayout: "horizontal", - buttonsVisible: false, - }, - sections: [ - { - title: "Variants", - display: { - }, - elements: [ - { - title: "List of Variants", - field: "variants", - type: "table", - display: { - defaultLayout: "vertical", - className: "", - style: "", - headerClassName: "", - headerStyle: "", - headerVisible: true, - // filter: array => array.filter(item => item.somatic), - // transform: array => array.map(item => { - // item.somatic = true; - // return item; - // }), - defaultValue: "-", - columns: [ - VariantTableFormatter.variantIdFormatter(), - VariantTableFormatter.geneFormatter(), - VariantTableFormatter.hgvsFormatter(gridConfig), - VariantTableFormatter.typeFormatter(), - VariantTableFormatter.consequenceTypeFormatter("", gridConfig), - { - title: "Deleteriousness", - display: { - columns: [ - VariantTableFormatter.siftFormatter(), - VariantTableFormatter.polyphenFormatter(), - VariantTableFormatter.revelFormatter(), - VariantTableFormatter.caddScaledFormatter(), - VariantTableFormatter.spliceAIFormatter(), - ] - } - }, - { - title: "Conservation", - display: { - columns: [ - VariantTableFormatter.conservationFormatter("PhyloP"), - VariantTableFormatter.conservationFormatter("PhastCons"), - VariantTableFormatter.conservationFormatter("GERP"), - ] - } - }, - // VariantTableFormatter.populationFrequencyFormatter(), - { - title: "Clinical", - display: { - columns: [ - VariantTableFormatter.clinvarFormatter(), - VariantTableFormatter.cosmicFormatter(), - ] - } - } - ], - }, - }, - ], - }, - ], - }; - } - } customElements.define("variant-browser-grid-test", VariantBrowserGridTest); From 5f6cf451b64a80a180a46639dee1ea8c024cf0b4 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 18:40:54 +0100 Subject: [PATCH 067/153] wc - Added data form table test component to enabledComponents and render Signed-off-by: gpveronica --- src/sites/test-app/test-app.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/sites/test-app/test-app.js b/src/sites/test-app/test-app.js index 22160850eb..4a781dcf81 100644 --- a/src/sites/test-app/test-app.js +++ b/src/sites/test-app/test-app.js @@ -35,6 +35,7 @@ import "../../webcomponents/commons/layouts/custom-sidebar.js"; import "../../webcomponents/commons/layouts/custom-welcome.js"; import "./webcomponents/data-form-test.js"; +import "./webcomponents/data-form-table-test.js"; import "./webcomponents/custom-page-test.js"; import "./webcomponents/variant-browser-grid-test.js"; import "./webcomponents/sample-browser-grid-test.js"; @@ -103,6 +104,7 @@ class TestApp extends LitElement { "login", "aboutzetta", "data-form", + "data-form-table", "utils-new", "catalog-filters", "file-browser-grid", @@ -643,6 +645,19 @@ class TestApp extends LitElement {
` : null} + ${this.config.enabledComponents["data-form-table"] ? html` +
+ + +
+ ` : null} + ${this.config.enabledComponents["utils-new"] ? html`
No component found. From 88ae0980226edd27c4120c844ace4d3e3b3163f2 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 18:41:20 +0100 Subject: [PATCH 068/153] wc - Added data form table component to menu Signed-off-by: gpveronica --- src/sites/test-app/conf/config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sites/test-app/conf/config.js b/src/sites/test-app/conf/config.js index 261425438c..f2576cd551 100644 --- a/src/sites/test-app/conf/config.js +++ b/src/sites/test-app/conf/config.js @@ -153,6 +153,12 @@ const SUITE = { description: "", visibility: "public" }, + { + id: "data-form-table", + name: "data-form Table Test", + description: "", + visibility: "public" + }, { id: "utils-new", name: "utils-new Test", From 8911c6377897bc20925865c6dbab659eabb75537 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Wed, 7 Feb 2024 18:45:18 +0100 Subject: [PATCH 069/153] wc - Refactored and fixed some tests Signed-off-by: gpveronica --- .../e2e/iva/variant-browser-grid-cancer.cy.js | 144 ++++++------------ 1 file changed, 49 insertions(+), 95 deletions(-) diff --git a/cypress/e2e/iva/variant-browser-grid-cancer.cy.js b/cypress/e2e/iva/variant-browser-grid-cancer.cy.js index d1feb85e3e..99f77a01d2 100644 --- a/cypress/e2e/iva/variant-browser-grid-cancer.cy.js +++ b/cypress/e2e/iva/variant-browser-grid-cancer.cy.js @@ -23,8 +23,10 @@ context("Variant Browser Grid Cancer", () => { beforeEach(() => { cy.visit("#variant-browser-grid-cancer"); + cy.get(`div[data-cy="variant-browser-container"]`) + .as("container"); cy.waitUntil(() => { - return cy.get(browserGrid) + return cy.get("@container") .should("be.visible"); }); }); @@ -116,119 +118,71 @@ context("Variant Browser Grid Cancer", () => { .should("be.visible"); }); - it("should chnage page variant-browser-grid", () => { + it("should change page variant-browser-grid", () => { UtilsTest.changePage(browserGrid,2); UtilsTest.changePage(browserGrid,3); }); }); - context("Tooltip", () => { - it("should display variant tooltip", () => { - // Select first row, first column: Variant - // variant == id - BrowserTest.getColumnIndexByHeader("Variant"); - cy.get("@indexColumn") - .then(index => { - cy.get("tbody tr:first > td") - .eq(index) - .within(() => { - cy.get("a") - .eq(0) - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); - }); + context("Variant Browser Bootstrap Grid", () => { + + beforeEach(() => { + cy.get("@container") + .find(`div[data-cy="vb-grid"]`) + .as("grid"); }); - it("should display gene tooltip", () => { - BrowserTest.getColumnIndexByHeader("Gene"); - cy.get("@indexColumn") - .then(index => { - cy.get("tbody tr:first > td") - .eq(index) - .within(() => { - cy.get("a") - .eq(0) - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); + context("Tooltip", () => { + + const tooltips = [ + {title: "Variant", }, + {title: "Gene"}, + {title: "Consequence Type"}, + ]; + const helps = [ + {title: "Deleteriousness"}, + {title: "Conservation"}, + {title: "Population Frequencies"}, + {title: "Clinical Info"}, + ]; + + beforeEach(() => { + cy.get("@grid") + .find("tbody") + .as("body"); }); - }); - it("should display consequenceType tooltip", () => { - BrowserTest.getColumnIndexByHeader("Consequence Type"); - cy.get("@indexColumn") - .then(index => { - cy.get("tbody tr:first > td") - .eq(index) + it("should display headers' help", () => { + // Select first row, first column: Variant + // variant == id + cy.wrap(helps).each(help => { + cy.get("@grid") + .contains("th", help.title) .within(() => { cy.get("a") - .eq(0) .trigger("mouseover"); }); cy.get(".qtip-content") .should("be.visible"); }); - }); - - it("should display population frequencies tooltip", () => { - cy.get("tbody tr:first > td") - .eq(13) - .within(() => { - cy.get("a") - .eq(0) - .trigger("mouseover"); }); - cy.get(".qtip-content") - .should("be.visible"); - }); - }); - context("Helpers", () => { - it("should display deleteriousness help", () => { - cy.get("thead th") - .contains("div","Deleteriousness") - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); - }); - - it("should display conservation help", () => { - cy.get("thead th") - .contains("div","Conservation") - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); - }); - - it("should display population frequencies help", () => { - cy.get("thead th") - .contains("div","Population Frequencies") - .within(() => { - cy.get("a") - .trigger("mouseover"); - }); - cy.get(".qtip-content") - .should("be.visible"); - }); - - it("should display clinical info help", () => { - cy.get("thead th") - .contains("div","Clinical Info") - .within(() => { - cy.get("a") - .trigger("mouseover"); + it("should display content tooltips", () => { + // Select first row, first column: Variant + // variant == id + cy.wrap(tooltips).each(tooltip => { + cy.get("@grid") + .contains("th", tooltip.title) + .invoke("index") + .then(i => { + cy.get("@body") + .find(`tr:first td:nth-child(${i+1}) a`) + .trigger("mouseover"); + cy.get(".qtip-content") + .should("be.visible"); + }); + }); }); - cy.get(".qtip-content") - .should("be.visible"); }); }); From 01843844e54ecc3a28aadae757a0b9867d5789b7 Mon Sep 17 00:00:00 2001 From: Josemi Date: Tue, 13 Feb 2024 12:51:17 +0100 Subject: [PATCH 070/153] wc: add view buttons in variant interpreter browser template #TASK-5636 --- .../variant-interpreter-browser-template.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index c3cd062699..4da28cfeea 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -77,6 +77,9 @@ class VariantInterpreterBrowserTemplate extends LitElement { this.notSavedVariantIds = 0; this.removedVariantIds = 0; + // Saves the current active view + this.activeView = "table"; + // Variant inclusion list this.variantInclusionState = []; @@ -345,6 +348,11 @@ class VariantInterpreterBrowserTemplate extends LitElement { this.requestUpdate(); } + onChangeView(newView) { + this.activeView = newView; + this.requestUpdate(); + } + render() { // Check Project exists if (!this.opencgaSession?.study) { @@ -435,6 +443,23 @@ class VariantInterpreterBrowserTemplate extends LitElement {
+ +
Date: Tue, 13 Feb 2024 12:56:01 +0100 Subject: [PATCH 071/153] wc: improve rendering views buttons in variant interpreter browser template #TASK-5636 --- .../variant-interpreter-browser-template.js | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 4da28cfeea..8c8a85fbed 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -353,6 +353,15 @@ class VariantInterpreterBrowserTemplate extends LitElement { this.requestUpdate(); } + renderViewButton(id, title, icon) { + return html` + + `; + } + render() { // Check Project exists if (!this.opencgaSession?.study) { @@ -443,21 +452,10 @@ class VariantInterpreterBrowserTemplate extends LitElement {
- " tooltip-position-at="left bottom" tooltip-position-my="right top">`, rowspan: 1, - colspan: 3, + colspan: 4, align: "center" }, { @@ -936,6 +936,16 @@ export default class VariantInterpreterGrid extends LitElement { align: "center", visible: !this._config.hideClinicalInfo && this.gridCommons.isColumnVisible("hotspots", "clinicalInfo"), }, + { + id: "omim", + title: "OMIM", + field: "omim", + colspan: 1, + rowspan: 1, + formatter: VariantGridFormatter.clinicalOmimFormatter, + align: "center", + visible: this.gridCommons.isColumnVisible("omim"), + }, // Interpretation methods column { id: "exomiser", diff --git a/src/webcomponents/variant/variant-browser-grid.js b/src/webcomponents/variant/variant-browser-grid.js index 2939d6d1ca..1bad34ff84 100644 --- a/src/webcomponents/variant/variant-browser-grid.js +++ b/src/webcomponents/variant/variant-browser-grid.js @@ -755,7 +755,7 @@ export default class VariantBrowserGrid extends LitElement { tooltip-position-at="left bottom" tooltip-position-my="right top">`, field: "clinicalInfo", rowspan: 1, - colspan: 2, + colspan: 3, align: "center" }, // ...ExtensionsManager.getColumns("variant-browser-grid"), @@ -968,6 +968,16 @@ export default class VariantBrowserGrid extends LitElement { align: "center", visible: this.gridCommons.isColumnVisible("cosmic", "clinicalInfo") }, + { + id: "omim", + title: "OMIM", + field: "omim", + colspan: 1, + rowspan: 1, + formatter: VariantGridFormatter.clinicalOmimFormatter, + align: "center", + visible: this.gridCommons.isColumnVisible("omim"), + }, ] ]; diff --git a/src/webcomponents/variant/variant-grid-formatter.js b/src/webcomponents/variant/variant-grid-formatter.js index 177c4c0620..ea273eb996 100644 --- a/src/webcomponents/variant/variant-grid-formatter.js +++ b/src/webcomponents/variant/variant-grid-formatter.js @@ -1503,4 +1503,35 @@ export default class VariantGridFormatter { return "-"; } + + static clinicalOmimFormatter(value, row) { + const entries = (row?.annotation?.geneTraitAssociation || []) + .filter(item => (item?.id || "").startsWith("OMIM:")) + .map(item => item.id.replace("OMIM:", "")); + + if (entries.length > 0) { + const uniqueEntries = new Set(entries); + const entriesLinks = Array.from(uniqueEntries).map(entry => { + return ` +
+ ${entry} +
+ `; + }); + const tooltipText = entriesLinks.join(""); + + return ` + + ${uniqueEntries.size}
${uniqueEntries.size === 1 ? "entry" : "entries"}
+
+ `; + } else { + return ` + + + + `; + } + } + } From 66817704c6a6ac64caf19f560bf283f8ff96fa0b Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 15 Feb 2024 10:05:51 +0100 Subject: [PATCH 081/153] wc - Ongoing changes for catalog --- src/core/utils-new.js | 3 +- .../clinical/clinical-analysis-view.js | 12 +- src/webcomponents/cohort/cohort-view.js | 76 +++-- src/webcomponents/commons/forms/data-form.js | 23 +- .../commons/forms/pdf-builder.js | 21 +- .../disease-panel/disease-panel-summary.js | 78 ++--- src/webcomponents/family/family-view.js | 62 ++-- src/webcomponents/file/file-view.js | 36 ++- .../individual/individual-view.js | 45 ++- src/webcomponents/job/job-view.js | 276 ++++++++++++------ src/webcomponents/sample/sample-view.js | 82 +----- 11 files changed, 403 insertions(+), 311 deletions(-) diff --git a/src/core/utils-new.js b/src/core/utils-new.js index a818cd55dd..9e5339c51a 100644 --- a/src/core/utils-new.js +++ b/src/core/utils-new.js @@ -290,7 +290,8 @@ export default class UtilsNew { static dateFormatter(date, format) { const _format = format ? format : "D MMM YYYY"; - return moment(date, "YYYYMMDDHHmmss").format(_format); + const _date = moment(date).isValid() ? moment(date) : moment(date, "YYYYMMDDHHmmss"); + return _date.format(_format); } static arrayDimension(array) { diff --git a/src/webcomponents/clinical/clinical-analysis-view.js b/src/webcomponents/clinical/clinical-analysis-view.js index b766f2ca77..bc8348f20b 100644 --- a/src/webcomponents/clinical/clinical-analysis-view.js +++ b/src/webcomponents/clinical/clinical-analysis-view.js @@ -234,12 +234,14 @@ export default class ClinicalAnalysisView extends LitElement { field: "proband.id", }, { - title: "Disorder", - field: "disorder", - id: "type", - type: "custom", + title: "Disorders", + type: "complex", display: { - render: disorder => UtilsNew.renderHTML(CatalogGridFormatter.disorderFormatter([disorder])), + template: "${disorder}", + format: { + disorder: disorder => CatalogGridFormatter.disorderFormatter([disorder]), + }, + defaultValue: "N/A", }, }, { diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index 5725e7de0d..0d877beb8d 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -141,11 +141,13 @@ export default class CohortView extends LitElement { } return html` -
@@ -270,79 +393,68 @@ export default class JobView extends LitElement { }, }, }, + { + name: "Input Parameters 2", + field: "params", + type: "list", + display: { + contentLayout: "bullets", + transform: values => Object + .entries(values) + // .map(([key, value]) => ({key, value})), + .map(([key, content]) => { + const value = (content && typeof content === "object") ? + JSON.stringify(content, null, 8) : + content; + return {key, value}; + }), + format: param => `${param.key}: ${param.value}`, + // format: param => this.jobParamFormatter(param), + // template: "${key}: ${value}", + // style: { + // key: { + // "font-weight": "bold" + // } + // } + } + }, { name: "Input Files", field: "input", type: "list", defaultValue: "N/A", display: { - template: "${name}", contentLayout: "bullets", + template: "${name}", }, }, { name: "Output Directory", field: "outDir.path", }, - { - name: "Output Files", - type: "custom", - display: { - render: job => { - // CAUTION: Temporary patch for managing outputFiles array of nulls. - // See details in: https://app.clickup.com/t/36631768/TASK-1704 - if (job.output.length > 0 && job.output.every(jobOut => jobOut === null)) { - return ` - - `; - } - const outputFiles= [...job.output]; - - // Check if stdout and stderr files have been created and can be dowloaded - ["stdout", "stderr"].forEach(file => { - if (job[file]?.id && job[file]?.type === "FILE") { - outputFiles.push(job[file]); - } - }); - - if (outputFiles.length === 0) { - return "No output files yet"; - } - - return `${outputFiles - .map(file => { - const url = [ - this.opencgaSession.server.host, - "/webservices/rest/", - this.opencgaSession.server.version, - "/files/", - file.id, - "/download?study=", - this.opencgaSession.study.fqn, - "&sid=", - this.opencgaSession.token, - ]; - return ` -
- ${file.name} ${file.size > 0 ? `(${UtilsNew.getDiskUsage(file.size)})` : ""} - - - -
`; - }) - .join("")}`; - }, - }, - }, + // { + // name: "Output Files", + // type: "complex", + // display: { + // // FIXME: export pdf not working + // template: "${output}", + // format: { + // "output": (output, data) => this.jobOutputFilesFormatter(output, data, this.opencgaSession), + // } + // }, + // }, { name: "Command Line", - type: "complex", + field: "commandLine", + // type: "text", + // text: data => data.commandLine, display: { - template: "
${commandLine}
", + className: "cmd", + // style: "display: block", + style: { + "display": "block" + }, + // textClassName: "cmd", }, }, ], diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index cb71089a18..335371ad24 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -15,7 +15,6 @@ */ import {LitElement, html} from "lit"; -import BioinfoUtils from "../../core/bioinfo/bioinfo-utils.js"; import LitUtils from "../commons/utils/lit-utils.js"; import UtilsNew from "../../core/utils-new.js"; import Types from "../commons/types.js"; @@ -23,7 +22,6 @@ import "../commons/forms/data-form.js"; import "../commons/filters/catalog-search-autocomplete.js"; import "../study/annotationset/annotation-set-view.js"; import "../loading-spinner.js"; -import PdfBuilder, {stylePdf} from "../commons/forms/pdf-builder"; import CatalogGridFormatter from "../commons/catalog-grid-formatter"; export default class SampleView extends LitElement { @@ -69,6 +67,7 @@ export default class SampleView extends LitElement { titleVisible: false, titleWidth: 2, defaultValue: "-", + pdf: true, }; this._config = this.getDefaultConfig(); } @@ -121,12 +120,6 @@ export default class SampleView extends LitElement { this.sampleId = e.detail.value; } - onDownloadPdf() { - const dataFormConf = this.getDefaultConfig(); - const pdfDocument = new PdfBuilder(this.sample, dataFormConf); - pdfDocument.exportToPdf(); - } - render() { if (this.isLoading) { return html``; @@ -142,11 +135,6 @@ export default class SampleView extends LitElement { } return html` - @@ -159,27 +147,6 @@ export default class SampleView extends LitElement { title: "Summary", icon: "", display: this.displayConfig || this.displayConfigDefault, - displayDoc: { - headerTitle: { - title: `Sample ${this.sample?.id}`, - display: { - classes: "h1", - propsStyle: { - ...stylePdf({ - alignment: "center", - bold: true, - }) - }, - }, - }, - watermark: { - text: "Demo", - color: "blue", - opacity: 0.3, - bold: true, - italics: false - }, - }, sections: [ { title: "Search", @@ -215,10 +182,14 @@ export default class SampleView extends LitElement { elements: [ { title: "Sample ID", - type: "custom", + type: "complex", display: { - visible: sample => sample?.id, - render: data => `${data.id} (UUID: ${data.uuid})`, + template: "${id} (UUID: ${uuid})", + style: { + id: { + "font-weight": "bold", + } + }, }, }, { @@ -247,26 +218,26 @@ export default class SampleView extends LitElement { }, { title: "Status", - field: "internal.status", - type: "custom", + type: "complex", display: { - render: field => `${field?.name} (${UtilsNew.dateFormatter(field?.date)})`, + template: "${internal.status.name} (${internal.status.date})", + format: { + "internal.status.date": date => UtilsNew.dateFormatter(date), + } }, }, { title: "Creation Date", field: "creationDate", - type: "custom", display: { - render: field => `${UtilsNew.dateFormatter(field)}`, + format: date => UtilsNew.dateFormatter(date), }, }, { title: "Modification Date", field: "modificationDate", - type: "custom", display: { - render: field => `${UtilsNew.dateFormatter(field)}`, + format: date => UtilsNew.dateFormatter(date), }, }, { @@ -283,30 +254,9 @@ export default class SampleView extends LitElement { display: { // showPDF: false, contentLayout: "bullets", - // render: phenotype => { - // let id = phenotype?.id; - // if (phenotype?.id?.startsWith("HP:")) { - // id = html` - // - // ${phenotype.id} - // - // `; - // } - // return phenotype?.name ? html`${phenotype.name} (${id})}` : html`${id}`; - // }, - format: phenotype => UtilsNew.renderHTML(CatalogGridFormatter.phenotypesFormatter([phenotype])), + format: phenotype => CatalogGridFormatter.phenotypesFormatter([phenotype]), }, }, - /* - { - title: "Annotation sets", - field: "annotationSets", - type: "custom", - display: { - render: field => html`` - } - } - */ ], }, ], From 4f319455ad6d01f9764bf4efead84ba90f6ef623 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 13:42:35 +0100 Subject: [PATCH 082/153] wc: remove GB tab in variant interpreter browser #TASK-5636 --- .../variant-interpreter-browser.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index 3c1c31acae..08d0a5e489 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -453,25 +453,6 @@ class VariantInterpreterBrowser extends LitElement { } } } - - // Append genome browser - if (this.settings.hideGenomeBrowser === undefined || this.settings.hideGenomeBrowser === false) { - items.push({ - id: "genome-browser", - name: "Genome Browser (Beta)", - render: (clinicalAnalysis, active, opencgaSession) => html` -
- - -
- `, - }); - } } // Return tabs configuration From 5567375ca4e1225b7dc0042186424f1cd9c0f4fa Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 13:48:12 +0100 Subject: [PATCH 083/153] wc: fix lint issues in interpreter browser #TAKS-5636 --- .../variant-interpreter-browser.js | 154 ++++++++++-------- 1 file changed, 83 insertions(+), 71 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index 08d0a5e489..b9869cebc4 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -292,6 +292,10 @@ class VariantInterpreterBrowser extends LitElement { name: "Variant Browser", active: true, render: (clinicalAnalysis, active, opencgaSession) => { + const browserSettings = { + ...this.settings.browsers["RD"], + hideGenomeBrowser: !!this.settings.hideGenomeBrowser, + }; return html`
@@ -321,6 +322,10 @@ class VariantInterpreterBrowser extends LitElement { name: "Somatic Small Variants", active: true, render: (clinicalAnalysis, active, opencgaSession) => { + const browserSettings = { + ...this.settings.browsers["CANCER_SNV"], + hideGenomeBrowser: !!this.settings.hideGenomeBrowser, + }; return html`
@@ -348,26 +350,29 @@ class VariantInterpreterBrowser extends LitElement { id: "somatic-cnv-variant-browser", name: "Somatic CNV Variants", active: false, - render: (clinicalAnalysis, active, opencgaSession) => html` -
- - - - -
- `, + render: (clinicalAnalysis, active, opencgaSession) => { + const browserSettings = { + ...this.settings.browsers["CANCER_CNV"], + hideGenomeBrowser: !!this.settings.hideGenomeBrowser, + }; + return html` +
+ + + + +
+ `; + }, }); } @@ -376,25 +381,28 @@ class VariantInterpreterBrowser extends LitElement { items.push({ id: "cancer-somatic-rearrangement-variant-browser", name: "Somatic Rearrangement Variants", - render: (clinicalAnalysis, active, opencgaSession) => html` -
- - - - -
- `, + render: (clinicalAnalysis, active, opencgaSession) => { + const browserSettings = { + ...this.settings.browsers["REARRANGEMENT"], + hideGenomeBrowser: !!this.settings.hideGenomeBrowser, + }; + return html` +
+ + + + +
+ `; + }, }); } @@ -404,6 +412,10 @@ class VariantInterpreterBrowser extends LitElement { id: "cancer-germline-variant-browser", name: "Germline Small Variants", render: (clinicalAnalysis, active, opencgaSession) => { + const browserSettings = { + ...this.settings.browsers["RD"], + hideGenomeBrowser: !!this.settings.hideGenomeBrowser, + }; return html`
@@ -429,26 +438,29 @@ class VariantInterpreterBrowser extends LitElement { items.push({ id: "rearrangement-germline-variant-browser", name: "Germline Rearrangement Variants", - render: (clinicalAnalysis, active, opencgaSession) => html` -
- - - - -
- `, + render: (clinicalAnalysis, active, opencgaSession) => { + const browserSettings = { + ...this.settings.browsers["REARRANGEMENT"], + hideGenomeBrowser: !!this.settings.hideGenomeBrowser, + }; + return html` +
+ + + + +
+ `; + }, }); } } From 0ed5653da89fe8b3fe914e8fbbeae60207472e39 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 13:50:16 +0100 Subject: [PATCH 084/153] wc: rename RD browser to Small Variants #TASK-5636 --- .../variant/interpretation/variant-interpreter-browser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index b9869cebc4..5ad420dd1b 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -289,7 +289,7 @@ class VariantInterpreterBrowser extends LitElement { if (type === "SINGLE" || type === "FAMILY") { items.push({ id: "variant-browser", - name: "Variant Browser", + name: "Small Variants", active: true, render: (clinicalAnalysis, active, opencgaSession) => { const browserSettings = { @@ -299,7 +299,7 @@ class VariantInterpreterBrowser extends LitElement { return html`
Date: Thu, 15 Feb 2024 13:54:21 +0100 Subject: [PATCH 085/153] wc: removed unused imports in interpreter-browser #TASK-5636 --- .../variant/interpretation/variant-interpreter-browser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index 5ad420dd1b..d3c81804e7 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -21,7 +21,6 @@ import "./variant-interpreter-browser-rd.js"; import "./variant-interpreter-browser-cancer.js"; import "./variant-interpreter-browser-cnv.js"; import "./variant-interpreter-browser-rearrangement.js"; -import "../../visualization/genome-browser.js"; import "../../commons/view/detail-tabs.js"; class VariantInterpreterBrowser extends LitElement { From e1f35c579a60e9f40a6b1f391c1549f9880ed105 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 17:08:50 +0100 Subject: [PATCH 086/153] wc: add default configuration for genome browser view in interpreter browser #TASK-5636 --- .../variant-interpreter-browser-template.js | 154 +++++++++++++++++- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 8eb3c0ab57..356d48b19d 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -18,7 +18,6 @@ import {html, LitElement} from "lit"; import VariantUtils from "../variant-utils.js"; import ClinicalAnalysisManager from "../../clinical/clinical-analysis-manager.js"; import LitUtils from "../../commons/utils/lit-utils.js"; -import NotificationUtils from "../../commons/utils/notification-utils.js"; import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; import UtilsNew from "../../../core/utils-new.js"; import "./variant-interpreter-browser-toolbar.js"; @@ -27,6 +26,8 @@ import "./variant-interpreter-detail.js"; import "../variant-browser-filter.js"; import "../../commons/tool-header.js"; import "../../commons/opencga-active-filters.js"; +import "../../visualization/genome-browser.js"; +import "../../visualization/split-genome-browser.js"; class VariantInterpreterBrowserTemplate extends LitElement { @@ -84,7 +85,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { this.variantInclusionState = []; this.currentQueryBeforeSaveEvent = null; - this._config = {}; + this._config = this.getDefaultConfig(); } connectedCallback() { @@ -143,7 +144,10 @@ class VariantInterpreterBrowserTemplate extends LitElement { return; } // merge filters - this._config = {...this.config}; + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; // filter list, canned filters, detail tabs if (this.settings?.menu) { @@ -526,7 +530,17 @@ class VariantInterpreterBrowserTemplate extends LitElement {
- Genome Browser + ${!this._config.filter.result.grid.isRearrangement ? html` + + + ` : html` + Split Genome Browser + `}
@@ -534,6 +548,138 @@ class VariantInterpreterBrowserTemplate extends LitElement { `; } + getDefaultConfig() { + let genomeBrowserTracks = []; + const genomeBrowserConfig = { + cellBaseClient: this.opencgaSession?.cellbaseClient, + featuresOfInterest: [], + }; + + // Check for opencgaSession and clinicalAnalysis defined + if (this.opencgaSession && this.clinicalAnalysis) { + const type = this.clinicalAnalysis.type.toUpperCase(); + + // Append tracks + genomeBrowserTracks = [ + { + type: "gene-overview", + overview: true, + config: {}, + }, + { + type: "sequence", + config: {}, + }, + { + type: "gene", + config: {}, + }, + { + type: "opencga-variant", + config: { + title: "Variants", + query: { + sample: this.clinicalAnalysis.proband.samples.map(s => s.id).join(","), + }, + }, + }, + ...(this.clinicalAnalysis.proband?.samples || []).map(sample => ({ + type: "opencga-alignment", + config: { + title: `Alignments - ${sample.id}`, + sample: sample.id, + }, + })), + ]; + + // Add interpretation panels to features of interest + if (this.clinicalAnalysis?.interpretation?.panels?.length > 0) { + genomeBrowserConfig.featuresOfInterest.push({ + name: "Panels of the interpretation", + category: true, + }); + + const colors = ["green", "blue", "darkorange", "blueviolet", "sienna", "indigo", "salmon"]; + const assembly = this.opencgaSession.project.organism?.assembly; + this.clinicalAnalysis.interpretation.panels.forEach((panel, index) => { + genomeBrowserConfig.featuresOfInterest.push({ + name: panel.name, + features: panel.genes + .map(gene => { + const coordinates = gene?.coordinates?.find(c => c.assembly === assembly); + if (!coordinates) { + return null; + } else { + const region = new Region(coordinates.location); + return { + chromosome: region.chromosome, + start: region.start, + end: region.end, + name: ` +
${gene.name}
+
${region.toString()}
+ `, + }; + } + }) + .filter(gene => !!gene) + .sort((a, b) => a.name < b.name ? -1 : +1), + display: { + visible: true, + color: colors[index % colors.length], + }, + }); + }); + } + + if (this.clinicalAnalysis.interpretation?.primaryFindings?.length > 0) { + if (genomeBrowserConfig.featuresOfInterest.length > 0) { + genomeBrowserConfig.featuresOfInterest.push({separator: true}); + } + genomeBrowserConfig.featuresOfInterest.push({ + name: "Variants", + category: true, + }); + genomeBrowserConfig.featuresOfInterest.push({ + name: "Primary Findings", + features: this.clinicalAnalysis.interpretation.primaryFindings.map(feature => { + const genes = Array.from(new Set(feature.annotation.consequenceTypes.filter(ct => !!ct.geneName).map(ct => ct.geneName))); + return { + id: feature.id, + chromosome: feature.chromosome, + start: feature.start, + end: feature.end ?? (feature.start + 1), + name: ` +
+
${feature.id} (${feature.type})
+ ${feature.annotation.displayConsequenceType ? ` +
+ ${feature.annotation.displayConsequenceType} +
+ ` : ""} + ${genes.length > 0 ? ` +
${genes.join(", ")}
+ ` : ""} +
+ `, + }; + }), + display: { + visible: true, + color: "red", + }, + }); + } + } + + return { + genomeBrowser: { + config: genomeBrowserConfig, + tracks: genomeBrowserTracks, + }, + }; + } + } customElements.define("variant-interpreter-browser-template", VariantInterpreterBrowserTemplate); From 60ff065a4a07989763404e65d8ad4781166dcc21 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 17:11:52 +0100 Subject: [PATCH 087/153] wc: minor improvements generating panels list for features of interest in genome browser view #TASK-5636 --- .../variant-interpreter-browser-template.js | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 356d48b19d..a61e12f66d 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -605,24 +605,20 @@ class VariantInterpreterBrowserTemplate extends LitElement { genomeBrowserConfig.featuresOfInterest.push({ name: panel.name, features: panel.genes + .filter(gene => !!gene?.coordinates?.find(c => c.assembly === assembly)) .map(gene => { const coordinates = gene?.coordinates?.find(c => c.assembly === assembly); - if (!coordinates) { - return null; - } else { - const region = new Region(coordinates.location); - return { - chromosome: region.chromosome, - start: region.start, - end: region.end, - name: ` -
${gene.name}
-
${region.toString()}
- `, - }; - } + const region = new Region(coordinates.location); + return { + chromosome: region.chromosome, + start: region.start, + end: region.end, + name: ` +
${gene.name}
+
${region.toString()}
+ `, + }; }) - .filter(gene => !!gene) .sort((a, b) => a.name < b.name ? -1 : +1), display: { visible: true, From 772c125635985be2658f49c10725573e139c585c Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 17:13:25 +0100 Subject: [PATCH 088/153] wc: add missing import in interpreter browser template #TASK-5636 --- .../interpretation/variant-interpreter-browser-template.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index a61e12f66d..28878e16a6 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -20,6 +20,7 @@ import ClinicalAnalysisManager from "../../clinical/clinical-analysis-manager.js import LitUtils from "../../commons/utils/lit-utils.js"; import OpencgaCatalogUtils from "../../../core/clients/opencga/opencga-catalog-utils.js"; import UtilsNew from "../../../core/utils-new.js"; +import Region from "../../../core/bioinfo/region.js"; import "./variant-interpreter-browser-toolbar.js"; import "./variant-interpreter-grid.js"; import "./variant-interpreter-detail.js"; @@ -605,7 +606,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { genomeBrowserConfig.featuresOfInterest.push({ name: panel.name, features: panel.genes - .filter(gene => !!gene?.coordinates?.find(c => c.assembly === assembly)) + .filter(gene => gene?.coordinates?.some(c => c.assembly === assembly)) .map(gene => { const coordinates = gene?.coordinates?.find(c => c.assembly === assembly); const region = new Region(coordinates.location); From 6070f3565bd33b9284dd886d1e734d59833303d6 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 15 Feb 2024 17:14:11 +0100 Subject: [PATCH 089/153] cy - Fixed and added tests Signed-off-by: gpveronica --- .../iva/variant-interpreter-grid-germline.cy.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js b/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js index 692c424bbf..6775a15fd2 100644 --- a/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js +++ b/cypress/e2e/iva/variant-interpreter-grid-germline.cy.js @@ -192,7 +192,7 @@ context("Variant Interpreter Grid Germiline", () => { it("should display ACMG Prediction (Classification) tooltip", () => { cy.get("tbody tr:first > td") - .eq(15) + .eq(16) .within(() => { cy.get("a") .trigger("mouseover"); @@ -200,6 +200,19 @@ context("Variant Interpreter Grid Germiline", () => { cy.get(".qtip-content") .should("be.visible"); }); + + it("should display OMIM Prediction (Classification) tooltip", () => { + UtilsTest.changePage(browserInterpreterGrid,2); + + cy.get("tbody tr:nth-child(6) > td:nth-child(15)") + .within(() => { + cy.get("a") + .trigger("mouseover"); + }); + cy.get(".qtip-content") + .should("be.visible"); + }); + }); context("Helpers", () => { @@ -236,7 +249,7 @@ context("Variant Interpreter Grid Germiline", () => { .should("be.visible"); }); - it("should display interpretation column hel`", () => { + it("should display interpretation column help", () => { cy.get("thead th") .contains("div","Interpretation") .within(() => { From 4f0e2473209330362e6d32e348294ae34b3552a0 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 17:18:14 +0100 Subject: [PATCH 090/153] wc: remove GB detail tab in rearrangement browser #TASK-5636 --- .../variant-interpreter-browser-rearrangement.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js index bca9d78222..393925b0f0 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js @@ -17,7 +17,6 @@ import {LitElement, html} from "lit"; import UtilsNew from "../../../core/utils-new.js"; import "./variant-interpreter-browser-template.js"; -import "../../visualization/split-genome-browser.js"; import "../../commons/json-viewer.js"; class VariantInterpreterBrowserRearrangement extends LitElement { @@ -444,20 +443,6 @@ class VariantInterpreterBrowserRearrangement extends LitElement { }, showTitle: true, items: [ - { - id: "browser", - name: "Rearrangements Browser", - active: true, - render: variants => html` - - - `, - }, { id: "json-view-variant1", name: "Variant 1 JSON Data", From 3bf570798378f10c56b8af13d91c3b5825481ba3 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 17:20:28 +0100 Subject: [PATCH 091/153] wc: fix providing configuration for genome browser view in rearrangement browser #TASK-5636 --- ...riant-interpreter-browser-rearrangement.js | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js index 393925b0f0..2fc658841d 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js @@ -283,37 +283,6 @@ class VariantInterpreterBrowserRearrangement extends LitElement { }); // Generate Genome browser columns and tracks configurations - const genomeBrowserTracks = [ - { - type: "gene", - config: {}, - }, - { - type: "opencga-variant", - config: { - title: "Variants", - query: { - sample: (this.clinicalAnalysis?.proband?.samples || []).map(s => s.id).join(","), - }, - height: 120, - }, - }, - ...(this.clinicalAnalysis?.proband?.samples || []).map(sample => ({ - type: "opencga-alignment", - config: { - title: `Alignments - ${sample.id}`, - sample: sample.id, - }, - })), - ]; - const genomeBrowserConfig = { - cellBaseClient: this.cellbaseClient, - karyotypePanelVisible: false, - overviewPanelVisible: false, - navigationPanelHistoryControlsVisible: false, - navigationPanelGeneSearchVisible: false, - navigationPanelRegionSearchVisible: false, - }; return { title: "Cancer Case Interpreter", @@ -467,6 +436,39 @@ class VariantInterpreterBrowserRearrangement extends LitElement { } }, aggregation: {}, + genomeBrowser: { + config: { + cellBaseClient: this.cellbaseClient, + karyotypePanelVisible: false, + overviewPanelVisible: false, + navigationPanelHistoryControlsVisible: false, + navigationPanelGeneSearchVisible: false, + navigationPanelRegionSearchVisible: false, + }, + tracks: [ + { + type: "gene", + config: {}, + }, + { + type: "opencga-variant", + config: { + title: "Variants", + query: { + sample: (this.clinicalAnalysis?.proband?.samples || []).map(s => s.id).join(","), + }, + height: 120, + }, + }, + ...(this.clinicalAnalysis?.proband?.samples || []).map(sample => ({ + type: "opencga-alignment", + config: { + title: `Alignments - ${sample.id}`, + sample: sample.id, + }, + })), + ], + }, }; } From cfd5d07372257f535c3369c04e1acaae38cddce0 Mon Sep 17 00:00:00 2001 From: Josemi Date: Thu, 15 Feb 2024 17:20:51 +0100 Subject: [PATCH 092/153] wc: add split genome browser component when browser is rearrangement #TAKS-5636 --- .../variant-interpreter-browser-template.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 28878e16a6..1c93d90f6d 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -540,7 +540,14 @@ class VariantInterpreterBrowserTemplate extends LitElement { .active="${this.activeView === "genome-browser"}"> ` : html` - Split Genome Browser + + + `}
From 1ccbb965275d75f849b9dd40ff88182076327c88 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 15 Feb 2024 23:25:32 +0100 Subject: [PATCH 093/153] wc - Fixed job view Signed-off-by: gpveronica --- src/webcomponents/job/job-view.js | 188 +++++++++--------------------- 1 file changed, 56 insertions(+), 132 deletions(-) diff --git a/src/webcomponents/job/job-view.js b/src/webcomponents/job/job-view.js index 2456b82ded..db8d829f59 100644 --- a/src/webcomponents/job/job-view.js +++ b/src/webcomponents/job/job-view.js @@ -64,7 +64,7 @@ export default class JobView extends LitElement { this.displayConfigDefault = { collapsable: true, titleVisible: false, - titleWidth: 2, + titleWidth: 3, defaultValue: "-", defaultLayout: "horizontal", buttonsVisible: false, @@ -78,7 +78,26 @@ export default class JobView extends LitElement { this.requestUpdate(); } + #prepareData() { + // 0. Local copy + this._job = UtilsNew.objectClone(this.job); + + // 1. Transform datapoint Input Parameters 'params' object into an array + if (this._job?.params && typeof this._job.params === "object") { + this._job.params = Object.entries(this._job.params) + .map(([paramKey, content]) => { + const paramValue = (content && typeof content === "object") ? + JSON.stringify(content, null, 8) : + content; + return {paramKey, paramValue}; + }); + } + } + update(changedProperties) { + if (changedProperties.has("job")) { + this.jobObserver(); + } if (changedProperties.has("jobId")) { this.jobIdObserver(); } @@ -89,12 +108,13 @@ export default class JobView extends LitElement { }; this._config = this.getDefaultConfig(); } - // if (changedProperties.has("opencgaSession")) { - // this._config = this.getDefaultConfig(); - // } super.update(changedProperties); } + jobObserver() { + this.#prepareData(); + } + jobIdObserver() { if (this.jobId && this.opencgaSession) { const params = { @@ -106,6 +126,7 @@ export default class JobView extends LitElement { .info(this.jobId, params) .then(response => { this.job = response.responses[0].results[0]; + this.#prepareData(); }) .catch(reason => { this.job = {}; @@ -127,39 +148,9 @@ export default class JobView extends LitElement { } // FORMATTERS - tagsFormatter(tag) { - return `${tag}`; - } - - jobStatusFormatter(status, appendDescription = false) { - const description = appendDescription && status.description ? `
${status.description}` : ""; - // FIXME remove this backward-compatibility check in next v2.3 - const statusId = status.id || status.name; - switch (statusId) { - case "PENDING": - case "QUEUED": - return ` ${statusId}${description}`; - case "RUNNING": - return ` ${statusId}${description}`; - case "DONE": - return ` ${statusId}${description}`; - case "ERROR": - return ` ${statusId}${description}`; - case "UNKNOWN": - return ` ${statusId}${description}`; - case "ABORTED": - return ` ${statusId}${description}`; - case "DELETED": - return ` ${statusId}${description}`; - } - return "-"; - } - - jobOutputFilesFormatter(output, job, opencgaSession) { // CAUTION: Temporary patch for managing outputFiles array of nulls. // See details in: https://app.clickup.com/t/36631768/TASK-1704 - debugger if (output.length > 0 && output.every(jobOut => jobOut === null)) { return ` From 63aa04b0cb4c656d7595050e9ccab969c569f8fb Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 16 Feb 2024 14:45:08 +0100 Subject: [PATCH 099/153] wc - Added className to dataform template --- src/webcomponents/commons/forms/data-form.js | 41 +++++++++----------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 75d8430942..e27a95c580 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -156,6 +156,7 @@ export default class DataForm extends LitElement { } else { value = defaultValue; } + return value; } @@ -164,7 +165,6 @@ export default class DataForm extends LitElement { const matches = template .match(/\$\{[a-zA-Z_.\[\]]+}/g) .map(elem => elem.substring(2, elem.length - 1)); -debugger for (const match of matches) { // Check if 'style' has been defined for this match variable, example: @@ -198,14 +198,10 @@ debugger const href = element?.display?.link?.[match](value, data); value = `${value}`; } - if (element?.display?.style?.[match]) { - const style = this._parseStyleField(element.display.style[match], value, data); - value = `${value}`; - } - // CAUTION New Vero: template each field with class names - if (element?.display?.classes?.[match]) { - const classes = element.display.classes[match]; - value = `${value}`; + if (element?.display?.className?.[match] || element?.display?.style?.[match]) { + const style = this._parseStyleField(element.display?.style?.[match], value, data); + const className = this._parseClassField(element.display?.className?.[match], value, data); + value = `${value}`; } // eslint-disable-next-line no-param-reassign @@ -235,6 +231,14 @@ debugger return style; } + _parseClassField(elementClass, value, data = this.data) { + let resultClass = elementClass || ""; + if (elementClass && typeof elementClass == "function") { + resultClass = elementClass(value, data); + } + return resultClass; + } + _getType() { // In case that we are using the deprecated form type, get type from display.mode.type return (this.config.type === "form") ? this.config.display?.mode?.type ?? "" : this.config.type ?? ""; @@ -1113,14 +1117,6 @@ debugger values = element.display.getData(data); } const contentLayout = element.display?.contentLayout || "vertical"; - - // 0. Apply 'transform' functions if defined - // CAUTION 20240214 VERO: Moved here to have a chance to tranform the values before - // applying array methods (e.g. for tranforming an object into an array of objects) - if (typeof element.display?.transform === "function") { - values = element.display.transform(values); - } - // 1. Check array and layout exist if (!Array.isArray(values)) { return this._createElementTemplate(element, null, null, { @@ -1140,9 +1136,10 @@ debugger values = element.display.filter(values); } - // if (typeof element.display?.transform === "function") { - // values = element.display.transform(values); - // } + if (typeof element.display?.transform === "function") { + debugger + values = element.display.transform(values); + } // 3. Check length of the array. This MUST be done after filtering if (values.length === 0) { @@ -1211,8 +1208,8 @@ debugger ${elem} ${index < values.length - 1 ? separators[index] ?? ", " : ""} `) - .join(``) - }`; + .join("")} + `; break; case "vertical": content = ` From 947467c7d5636711a381e3b42adcec79cac25bb7 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 16 Feb 2024 15:11:34 +0100 Subject: [PATCH 100/153] wc - Minor fix in class key Signed-off-by: gpveronica --- src/webcomponents/job/job-view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/job/job-view.js b/src/webcomponents/job/job-view.js index db8d829f59..6cff3a4bca 100644 --- a/src/webcomponents/job/job-view.js +++ b/src/webcomponents/job/job-view.js @@ -290,7 +290,7 @@ export default class JobView extends LitElement { contentLayout: "vertical", transform: tags => tags.map(tag => ({tag})), template: "${tag}", - classes: { + className: { "tag": "badge badge-pill badge-primary", }, }, From cf6cc4f925ed11286cd554d923cf6e592b8742e4 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 16 Feb 2024 15:12:38 +0100 Subject: [PATCH 101/153] wc - Removed debugger --- src/webcomponents/commons/forms/data-form.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index e27a95c580..f2e3fc70d6 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -1137,7 +1137,6 @@ export default class DataForm extends LitElement { } if (typeof element.display?.transform === "function") { - debugger values = element.display.transform(values); } From 2db83251b9fbdbad7aa64f1c06d8202f0ac00bef Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 16:41:22 +0100 Subject: [PATCH 102/153] wc: add active property to rearrangement grid #TASK-5636 --- .../variant-interpreter-rearrangement-grid.js | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 8c783ff801..6bf2f3a369 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -61,7 +61,10 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { }, config: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -73,6 +76,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.gridId = this._prefix + "VariantBrowserGrid"; this.checkedVariants = new Map(); this.review = false; + this.active = true; this.variantsReview = null; // OpenCGA returns the same genes in both variants of the rearrangement @@ -142,6 +146,10 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.requestUpdate(); this.renderVariants(); } + + if (changedProperties.has("active")) { + this.renderVariants(); + } } opencgaSessionObserver() { @@ -261,10 +269,12 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { } renderVariants() { - if (this.clinicalVariants && this.clinicalVariants.length > 0) { - this.renderLocalVariants(); - } else { - this.renderRemoteVariants(); + if (this.active) { + if (this.clinicalVariants && this.clinicalVariants.length > 0) { + this.renderLocalVariants(); + } else { + this.renderRemoteVariants(); + } } } From 03a63e16da0177c15578997b0e707fd8e806c8ea Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 16:43:31 +0100 Subject: [PATCH 103/153] wc: add missing config observer in rearrangement grid #TASK-5636 --- .../variant-interpreter-rearrangement-grid.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 6bf2f3a369..8f8dda87e2 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -127,22 +127,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { } if (changedProperties.has("config") || changedProperties.has("toolId")) { - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; - this.gridCommons = new GridCommons(this.gridId, this, this._config); - - this.toolbarSetting = { - ...this._config, - }; - - this.toolbarConfig = { - toolId: this.toolId, - resource: "CLINICAL_VARIANT", - showInterpreterConfig: true, - columns: this._getDefaultColumns() - }; + this.configObserver(); this.requestUpdate(); this.renderVariants(); } @@ -191,6 +176,25 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { } } + configObserver() { + this._config = { + ...this.getDefaultConfig(), + ...this.config, + }; + this.gridCommons = new GridCommons(this.gridId, this, this._config); + + this.toolbarSetting = { + ...this._config, + }; + + this.toolbarConfig = { + toolId: this.toolId, + resource: "CLINICAL_VARIANT", + showInterpreterConfig: true, + columns: this._getDefaultColumns() + }; + } + onColumnChange(e) { this.gridCommons.onColumnChange(e); } From 274cc8ead84631adfab4f44699179bfb71b07802 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 17:17:00 +0100 Subject: [PATCH 104/153] wc: simplify observers and update/updated methods in rearrangement grid #TASK-5636 --- .../variant-interpreter-rearrangement-grid.js | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 8f8dda87e2..26bc9f33fb 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -73,12 +73,17 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this._prefix = UtilsNew.randomString(8); this.toolbarConfig = {}; + this.toolbarSetting = {}; + this.gridId = this._prefix + "VariantBrowserGrid"; this.checkedVariants = new Map(); this.review = false; this.active = true; this.variantsReview = null; + this.gridCommons = null; + this.clinicalAnalysisManager = null; + // OpenCGA returns the same genes in both variants of the rearrangement // This map is used to assign the correct genes to each variant this.genesByVariant = {}; @@ -112,50 +117,25 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.COMPONENT_ID = this.toolId + "-grid"; } - super.update(changedProperties); - } - - updated(changedProperties) { - if (changedProperties.has("opencgaSession")) { - this.opencgaSessionObserver(); - } - - if (changedProperties.has("clinicalAnalysis") || changedProperties.has("query") || changedProperties.has("toolId")) { - // this.opencgaSessionObserver(); + if (changedProperties.has("clinicalAnalysis") || changedProperties.has("opencgaSession")) { this.clinicalAnalysisObserver(); - this.renderVariants(); } - if (changedProperties.has("config") || changedProperties.has("toolId")) { + if (changedProperties.has("config")) { this.configObserver(); - this.requestUpdate(); - this.renderVariants(); } - if (changedProperties.has("active")) { - this.renderVariants(); - } + super.update(changedProperties); } - opencgaSessionObserver() { - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; - this.gridCommons = new GridCommons(this.gridId, this, this._config); - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); + updated() { + this.renderVariants(); } clinicalAnalysisObserver() { - // We need to load server config always. - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); + if (this.opencgaSession && this.clinicalAnalysis) { + this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); - // Make sure somatic sample is the first one - if (this.clinicalAnalysis) { if (!this.clinicalAnalysis.interpretation) { this.clinicalAnalysis.interpretation = {}; } @@ -177,16 +157,21 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { } configObserver() { + // 1. Merge default configuration with the configuration provided via props this._config = { ...this.getDefaultConfig(), ...this.config, }; + + // 2. Initialize grid commons with the new configutation this.gridCommons = new GridCommons(this.gridId, this, this._config); + // 3. Set toolbar settings this.toolbarSetting = { ...this._config, }; + // 4. Set toolbar configuration this.toolbarConfig = { toolId: this.toolId, resource: "CLINICAL_VARIANT", From c7cb7cfd1c22aad13e2fd6d930cb5ecc70c0e2d7 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 17:18:22 +0100 Subject: [PATCH 105/153] wc: removed unused listeners in rearrangement grid #TASK-5636 --- .../variant-interpreter-rearrangement-grid.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 26bc9f33fb..c099d854b5 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -94,24 +94,6 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { this.consequenceTypeColors = VariantGridFormatter.assignColors(CONSEQUENCE_TYPES, PROTEIN_SUBSTITUTION_SCORE); } - connectedCallback() { - super.connectedCallback(); - - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; - this.gridCommons = new GridCommons(this.gridId, this, this._config); - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); - } - - firstUpdated() { - this.downloadRefreshIcon = $("#" + this._prefix + "DownloadRefresh"); - this.downloadIcon = $("#" + this._prefix + "DownloadIcon"); - this.table = $("#" + this.gridId); - // this.checkedVariants = new Map(); - } - update(changedProperties) { if (changedProperties.has("toolId")) { this.COMPONENT_ID = this.toolId + "-grid"; From 0f5cbaaa4c36aad2636d99d145873371960f1a69 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 17:28:09 +0100 Subject: [PATCH 106/153] wc: fix updated method of rearrangement grid #TASK-5636 --- .../variant-interpreter-rearrangement-grid.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index c099d854b5..72086a047a 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -110,8 +110,12 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { super.update(changedProperties); } - updated() { - this.renderVariants(); + updated(changedProperties) { + // We ned to perform an update of the table only when any of the properties of this grid has changed + // This means that we only need to check if the changedProperties set is not empty + if (changedProperties.size > 0) { + this.renderVariants(); + } } clinicalAnalysisObserver() { From 9519c0e555d76b029bbb5c36a1e8cc40df2d0338 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:04:54 +0100 Subject: [PATCH 107/153] wc: fixed bug rendering local variants in rearrangement grid #TASK-5636 --- .../interpretation/variant-interpreter-rearrangement-grid.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 72086a047a..18242781b9 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -241,6 +241,9 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { }); }); } + + // In other case, we will return a dummy promise + return Promise.resolve(null); } renderVariants() { @@ -372,7 +375,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { const rows = variants.slice(skip, skip + limit); // Generate map of genes to variants - this.generateGenesMapFromVariants(rows) + this.generateGenesMapFromVariants(rows.flat()) .then(() => params.success(rows)) .catch(error => params.error(error)); }, From 20fd90dfa285e921e75be612c20bf5d0df7dc0e3 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:08:27 +0100 Subject: [PATCH 108/153] wc: add active property to variant interpreter grid #TASK-5636 --- .../variant-interpreter-grid.js | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 0c32f1cd74..841b789afa 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -65,7 +65,10 @@ export default class VariantInterpreterGrid extends LitElement { }, config: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -74,6 +77,7 @@ export default class VariantInterpreterGrid extends LitElement { this._prefix = UtilsNew.randomString(8); this.gridId = this._prefix + "VariantBrowserGrid"; this.checkedVariants = new Map(); + this.active = true; // Set colors // eslint-disable-next-line no-undef @@ -106,22 +110,23 @@ export default class VariantInterpreterGrid extends LitElement { this.COMPONENT_ID = this.toolId + "-grid"; } - super.update(changedProperties); - } - - updated(changedProperties) { if (changedProperties.has("opencgaSession")) { this.opencgaSessionObserver(); } + if (changedProperties.has("clinicalAnalysis")) { this.clinicalAnalysisObserver(); } - if (changedProperties.has("query") || changedProperties.has("clinicalVariants")) { - this.renderVariants(); - } - if (changedProperties.has("config") || changedProperties.has("toolId")) { + + if (changedProperties.has("config")) { this.configObserver(); - this.requestUpdate(); + } + + super.update(changedProperties); + } + + updated(changedProperties) { + if (changedProperties.has("query") || changedProperties.has("clinicalVariants") || changedProperties.has("config") || changedProperties.has("toolId") || changedProperties.has("active")) { this.renderVariants(); } } @@ -194,20 +199,12 @@ export default class VariantInterpreterGrid extends LitElement { } renderVariants() { - if (this._config.renderLocal) { - // FIXME remove this ASAP - this.clinicalVariants = this.clinicalAnalysis.interpretation.primaryFindings; - } - - if (this.clinicalVariants?.length > 0) { - // FIXME Temporary code to check which variants are being interpreted or have been reported - // This should be implemented by OpenCGA - // this.fillReportedVariants(this.clinicalVariants) - // .catch(error => console.error(error)) - // .finally(() => this.renderLocalVariants()); - this.renderLocalVariants(); - } else { - this.renderRemoteVariants(); + if (this.active) { + if (this.clinicalVariants?.length > 0) { + this.renderLocalVariants(); + } else { + this.renderRemoteVariants(); + } } } From a21bf9c51bc50d31354709e42f8ee3f435b9b2ca Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:09:20 +0100 Subject: [PATCH 109/153] wc: fix condition to check if we sould render grid table again in the updated listener of interpreter grid #TASK-5636 --- .../variant/interpretation/variant-interpreter-grid.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 841b789afa..c25534746e 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -126,7 +126,9 @@ export default class VariantInterpreterGrid extends LitElement { } updated(changedProperties) { - if (changedProperties.has("query") || changedProperties.has("clinicalVariants") || changedProperties.has("config") || changedProperties.has("toolId") || changedProperties.has("active")) { + // We ned to perform an update of the table only when any of the properties of this grid has changed + // This means that we only need to check if the changedProperties set is not empty + if (changedProperties.size > 0) { this.renderVariants(); } } From 4d86ed03b3923128f2b79a906f26fdb443def5a1 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:13:27 +0100 Subject: [PATCH 110/153] wc: add missing initialization of internal config in rearrangement grid #TASK-5636 --- .../interpretation/variant-interpreter-rearrangement-grid.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index 18242781b9..df6695d927 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -71,6 +71,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { _init() { this.COMPONENT_ID = ""; this._prefix = UtilsNew.randomString(8); + this._config = this.getDefaultConfig(); this.toolbarConfig = {}; this.toolbarSetting = {}; From 42a618bb9008568f73b645693384a57ef0303d64 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:17:29 +0100 Subject: [PATCH 111/153] wc: minor code style fixes in rearrangement grid #TASK-5636 --- .../variant-interpreter-rearrangement-grid.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js index df6695d927..9338aa2f80 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-rearrangement-grid.js @@ -31,8 +31,7 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { constructor() { super(); - - this._init(); + this.#init(); } createRenderRoot() { @@ -68,10 +67,11 @@ export default class VariantInterpreterRearrangementGrid extends LitElement { }; } - _init() { + #init() { this.COMPONENT_ID = ""; this._prefix = UtilsNew.randomString(8); this._config = this.getDefaultConfig(); + this._rows = []; this.toolbarConfig = {}; this.toolbarSetting = {}; From 178275b212cb13bd228892fe695870504c03758a Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:18:09 +0100 Subject: [PATCH 112/153] wc: remove unused functions and fix initialized variables in interpreter grid #TASK-5636 --- .../variant-interpreter-grid.js | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index c25534746e..22995807ba 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -75,18 +75,26 @@ export default class VariantInterpreterGrid extends LitElement { #init() { this.COMPONENT_ID = ""; this._prefix = UtilsNew.randomString(8); - this.gridId = this._prefix + "VariantBrowserGrid"; + this._config = this.getDefaultConfig(); + this._rows = []; + + this.toolbarConfig = {}; + this.toolbarSetting = {}; + this.checkedVariants = new Map(); + this.gridId = this._prefix + "VariantBrowserGrid"; this.active = true; + this.review = false; + + this.gridCommons = null; + this.clinicalAnalysisManager = null; // Set colors // eslint-disable-next-line no-undef this.consequenceTypeColors = VariantGridFormatter.assignColors(CONSEQUENCE_TYPES, PROTEIN_SUBSTITUTION_SCORE); // Keep the status of selected variants - this._rows = []; this.queriedVariants = {}; - this.review = false; this.displayConfigDefault = { header: { @@ -94,15 +102,6 @@ export default class VariantInterpreterGrid extends LitElement { verticalAlign: "bottom", }, }; - - } - - firstUpdated() { - this.table = this.querySelector("#" + this.gridId); - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; } update(changedProperties) { From bd40d0b49be07ed02b262e086ce89c937f6789c7 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:21:07 +0100 Subject: [PATCH 113/153] wc: improve properties observers of variant interpreter grid #TASK-5636 --- .../variant-interpreter-grid.js | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index 22995807ba..feb98e67c3 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -109,11 +109,7 @@ export default class VariantInterpreterGrid extends LitElement { this.COMPONENT_ID = this.toolId + "-grid"; } - if (changedProperties.has("opencgaSession")) { - this.opencgaSessionObserver(); - } - - if (changedProperties.has("clinicalAnalysis")) { + if (changedProperties.has("clinicalAnalysis") || changedProperties.has("opencgaSession")) { this.clinicalAnalysisObserver(); } @@ -132,25 +128,10 @@ export default class VariantInterpreterGrid extends LitElement { } } - opencgaSessionObserver() { - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; - this.gridCommons = new GridCommons(this.gridId, this, this._config); - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); - } - clinicalAnalysisObserver() { - // We need to load server config always. - this._config = { - ...this.getDefaultConfig(), - ...this.config, - }; - this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); + if (this.opencgaSession && this.clinicalAnalysis) { + this.clinicalAnalysisManager = new ClinicalAnalysisManager(this, this.clinicalAnalysis, this.opencgaSession); - // Make sure somatic sample is the first one - if (this.clinicalAnalysis) { if (!this.clinicalAnalysis.interpretation) { this.clinicalAnalysis.interpretation = {}; } @@ -174,19 +155,22 @@ export default class VariantInterpreterGrid extends LitElement { } configObserver() { + // 1. Merge default configuration with the configuration provided via props this._config = { ...this.getDefaultConfig(), ...this.config }; + + // 2. Create a new grid commons instance with the new configuration this.gridCommons = new GridCommons(this.gridId, this, this._config); + // 3. Set toolbar settings this.toolbarSetting = { ...this._config, showCreate: false, - // columns: defaultColumns[0].filter(col => col.rowspan === 2 && col.colspan === 1 && col.visible !== false), - // gridColumns: defaultColumns, // original column structure }; + // 4. Set toolbar config this.toolbarConfig = { toolId: this.toolId, resource: "CLINICAL_VARIANT", From 4ca14fe06c08c25246f52a6ff2c44ac3e3f3e133 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:31:27 +0100 Subject: [PATCH 114/153] wc: add active property to all interpreter browsers #TASK-5636 --- .../variant/interpretation/variant-interpreter-browser.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index d3c81804e7..32f3abd08f 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -305,6 +305,7 @@ class VariantInterpreterBrowser extends LitElement { .clinicalAnalysis="${clinicalAnalysis}" .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" + .active="${active}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}" @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @samplechange="${this.onSampleChange}"> @@ -335,6 +336,7 @@ class VariantInterpreterBrowser extends LitElement { .clinicalAnalysis="${clinicalAnalysis}" .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" + .active="${active}" @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}"> @@ -366,6 +368,7 @@ class VariantInterpreterBrowser extends LitElement { .query="${this.query}" .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" + .active="${active}" @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}"> @@ -396,7 +399,7 @@ class VariantInterpreterBrowser extends LitElement { .clinicalAnalysis="${clinicalAnalysis}" .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" - ?active="${active}" + .active="${active}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}"> @@ -426,6 +429,7 @@ class VariantInterpreterBrowser extends LitElement { .clinicalAnalysis="${clinicalAnalysis}" .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" + .active="${active}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}" @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @samplechange="${this.onSampleChange}"> @@ -454,7 +458,7 @@ class VariantInterpreterBrowser extends LitElement { .somatic="${false}" .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" - ?active="${active}" + .active="${active}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}"> From 97302396d61fa6306f7403ca8df121473140bc63 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:32:31 +0100 Subject: [PATCH 115/153] wc: add active property in interpreter browser cancer #TASK-5636 --- .../interpretation/variant-interpreter-browser-cancer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cancer.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cancer.js index 10f65267d0..b1bf709721 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cancer.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cancer.js @@ -53,7 +53,10 @@ class VariantInterpreterBrowserCancer extends LitElement { }, settings: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -269,6 +272,7 @@ class VariantInterpreterBrowserCancer extends LitElement { .settings="${this.settings}" .toolId="${this.COMPONENT_ID}" .config="${this._config}" + .active="${this.active}" @queryChange="${this.onQueryChange}"> `; From 327c46b233a883d130d56a438a7a00609e922f51 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:32:43 +0100 Subject: [PATCH 116/153] wc: add active property in interpreter browser cnv #TASK-5636 --- .../interpretation/variant-interpreter-browser-cnv.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js index 5a63c2c51e..bc942290ef 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-cnv.js @@ -50,7 +50,10 @@ class VariantInterpreterBrowserCNV extends LitElement { }, settings: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -231,6 +234,7 @@ class VariantInterpreterBrowserCNV extends LitElement { .settings="${this.settings}" .toolId="${this.COMPONENT_ID}" .config="${this._config}" + .active="${this.active}" @queryChange="${this.onQueryChange}"> `; From 4d39c7681297674041b91f855c90cf7b5365e089 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:32:58 +0100 Subject: [PATCH 117/153] wc: add active property in interpreter browser rd #TASK-5636 --- .../interpretation/variant-interpreter-browser-rd.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js index 9bb2199a90..9b0582eef0 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rd.js @@ -53,7 +53,10 @@ class VariantInterpreterBrowserRd extends LitElement { }, settings: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -285,6 +288,7 @@ class VariantInterpreterBrowserRd extends LitElement { .settings="${this.settings}" .toolId="${this.COMPONENT_ID}" .config="${this._config}" + .active="${this.active}" @queryChange="${this.onQueryChange}"> `; From 35dd8ed411b07264f8e3b3eff5524476a13c5a51 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:33:15 +0100 Subject: [PATCH 118/153] wc: add active property in interpreter browser rearrangement #TASK-5636 --- .../interpretation/variant-interpreter-browser-rearrangement.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js index 2fc658841d..9bb59a2517 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js @@ -259,6 +259,7 @@ class VariantInterpreterBrowserRearrangement extends LitElement { .settings="${this.settings}" .toolId="${this.COMPONENT_ID}" .config="${this._config}" + .active="${this.active}" @queryChange="${this.onQueryChange}"> `; From 704880a88a267193487fdbb5c9766bff653713c5 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:33:52 +0100 Subject: [PATCH 119/153] wc: add active property in interpreter browser template and use it on interpreter grids and GB #TASK-5636 --- .../variant-interpreter-browser-template.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 1c93d90f6d..abeac5222a 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -65,7 +65,10 @@ class VariantInterpreterBrowserTemplate extends LitElement { }, config: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -499,6 +502,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { .query="${this.executedQuery}" .review="${true}" .config="${this._config.filter.result.grid}" + .active="${this.active}" @queryComplete="${this.onQueryComplete}" @selectrow="${this.onSelectVariant}" @updaterow="${this.onUpdateVariant}" @@ -512,6 +516,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { .query="${this.executedQuery}" .review="${true}" .config="${this._config.filter.result.grid}" + .active="${this.active}" @queryComplete="${this.onQueryComplete}" @selectrow="${this.onSelectVariant}" @updaterow="${this.onUpdateVariant}" @@ -537,7 +542,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { .config="${this._config.genomeBrowser.config}" .region="${this.variant}" .tracks="${this._config.genomeBrowser.tracks}" - .active="${this.activeView === "genome-browser"}"> + .active="${this.active && this.activeView === "genome-browser"}"> ` : html` + .active="${this.active && this.activeView === "genome-browser"}"> `} From cda98eff28f420ea619e54e6fc3f49620b6815a7 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:34:35 +0100 Subject: [PATCH 120/153] wc: add active property and forward it to grids in interpreter review #TASK-5636 --- .../interpretation/variant-interpreter-review-primary.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js b/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js index 9a84eb7d0e..02afba6744 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js @@ -75,7 +75,10 @@ export default class VariantInterpreterReviewPrimary extends LitElement { }, config: { type: Object - } + }, + active: { + type: Boolean, + }, }; } @@ -276,6 +279,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { .clinicalAnalysis="${this.clinicalAnalysis}" .clinicalVariants="${this.clinicalVariants}" .review="${true}" + .active="${this.active}" .config="${this._config.result.grid}" @selectrow="${this.onSelectVariant}" @updaterow="${this.onUpdateVariant}" @@ -289,6 +293,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { .clinicalAnalysis="${this.clinicalAnalysis}" .clinicalVariants="${this.clinicalVariants}" .review="${true}" + .active="${this.active}" .config="${this._config.result.grid}" @selectrow="${this.onSelectVariant}" @updaterow="${this.onUpdateVariant}" From f7ad71d8b7ba9411f98a135b771f8d381cc594e5 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 16 Feb 2024 18:36:19 +0100 Subject: [PATCH 121/153] wc: fix split-genome-browser component when regions property is not defined #TASK-5636 --- src/webcomponents/visualization/split-genome-browser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/visualization/split-genome-browser.js b/src/webcomponents/visualization/split-genome-browser.js index 2013991edd..8cb5bbf5da 100644 --- a/src/webcomponents/visualization/split-genome-browser.js +++ b/src/webcomponents/visualization/split-genome-browser.js @@ -31,7 +31,7 @@ export default class SplitGenomeBrowser extends LitElement { } render() { - if (!this.opencgaSession) { + if (!this.opencgaSession || !this.regions) { return null; } From 91315e4acf14275d3f2967652f04b0013f3b6d75 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:44:39 +0100 Subject: [PATCH 122/153] wc: remove link to Genome Browser tool from actions dropdown in interpreter grid #TAKS-5636 --- .../variant/interpretation/variant-interpreter-grid.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index feb98e67c3..e27539cfb9 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -776,14 +776,6 @@ export default class VariantInterpreterGrid extends LitElement { - ${this._config.showGenomeBrowserLink ? ` - -
  • - - Genome Browser - -
  • - ` : ""}
  • Date: Mon, 19 Feb 2024 12:45:50 +0100 Subject: [PATCH 123/153] wc: remove genome browser action listener from interpreter grid #TASK-5636 --- .../variant/interpretation/variant-interpreter-grid.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js index e27539cfb9..68a182525f 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-grid.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-grid.js @@ -1149,11 +1149,6 @@ export default class VariantInterpreterGrid extends LitElement { $(`#${this._prefix}ReviewSampleModal`).modal("show"); } break; - case "genome-browser": - LitUtils.dispatchCustomEvent(this, "genomeBrowserRegionChange", null, { - region: row.chromosome + ":" + row.start + "-" + row.end, - }); - break; case "copy-json": UtilsNew.copyToClipboard(JSON.stringify(row, null, "\t")); break; From 5fd0dc65c88748a25322f7596ccb2c87dd7cc1be Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:48:20 +0100 Subject: [PATCH 124/153] wc: remove genome browser action listener from variant browser grid #TASK-5636 --- src/webcomponents/variant/variant-browser-grid.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/webcomponents/variant/variant-browser-grid.js b/src/webcomponents/variant/variant-browser-grid.js index 2939d6d1ca..409b5c5da6 100644 --- a/src/webcomponents/variant/variant-browser-grid.js +++ b/src/webcomponents/variant/variant-browser-grid.js @@ -981,11 +981,6 @@ export default class VariantBrowserGrid extends LitElement { onActionClick(e, value, row) { const action = e.target.dataset.action?.toLowerCase(); switch (action) { - case "genome-browser": - LitUtils.dispatchCustomEvent(this, "genomeBrowserRegionChange", null, { - region: row.chromosome + ":" + row.start + "-" + row.end, - }); - break; case "copy-json": navigator.clipboard.writeText(JSON.stringify(row, null, "\t")); break; From b3d316866a6af6747f9e3259a82f22671b1dea8b Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:49:49 +0100 Subject: [PATCH 125/153] wc: remove listeners to genome browser in interpreter browser #TASK-5636 --- .../variant-interpreter-browser.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index 32f3abd08f..3c09728aea 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -59,7 +59,6 @@ class VariantInterpreterBrowser extends LitElement { _init() { this._prefix = UtilsNew.randomString(8); this._activeTab = null; - this._genomeBrowserRegion = null; this._config = this.getDefaultConfig(); } @@ -103,20 +102,6 @@ class VariantInterpreterBrowser extends LitElement { } } - // onClinicalAnalysisUpdate(e) { - // LitUtils.dispatchCustomEvent(this, "clinicalAnalysisUpdate", null, { - // clinicalAnalysis: e.detail.clinicalAnalysis, - // }, null); - // } - - onGenomeBrowserRegionChange(event) { - this._genomeBrowserRegion = event.detail.region; - this._config = this.getDefaultConfig(); - this._activeTab = "genome-browser"; - - this.requestUpdate(); - } - onActiveTabChange(event) { this._activeTab = event.detail.value; this.requestUpdate(); @@ -307,7 +292,6 @@ class VariantInterpreterBrowser extends LitElement { .settings="${browserSettings}" .active="${active}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}" - @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @samplechange="${this.onSampleChange}"> @@ -337,7 +321,6 @@ class VariantInterpreterBrowser extends LitElement { .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" .active="${active}" - @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}"> @@ -369,7 +352,6 @@ class VariantInterpreterBrowser extends LitElement { .cellbaseClient="${this.cellbaseClient}" .settings="${browserSettings}" .active="${active}" - @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}"> @@ -431,7 +413,6 @@ class VariantInterpreterBrowser extends LitElement { .settings="${browserSettings}" .active="${active}" @clinicalAnalysisUpdate="${this.onClinicalAnalysisUpdate}" - @genomeBrowserRegionChange="${e => this.onGenomeBrowserRegionChange(e)}" @samplechange="${this.onSampleChange}"> From db94281c3d1b10d0ca55f5aedee04d4b11a86a37 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:51:06 +0100 Subject: [PATCH 126/153] wc: remove configuration for genome browser in variant interprerter browser #TASK-5636 --- .../variant-interpreter-browser.js | 118 ------------------ 1 file changed, 118 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js index 3c09728aea..fe25a59ace 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser.js @@ -149,127 +149,9 @@ class VariantInterpreterBrowser extends LitElement { getDefaultConfig() { const items = []; - // Check for clinicalAnalysis if (this.clinicalAnalysis) { const type = this.clinicalAnalysis.type.toUpperCase(); - // Genome browser configuration - const genomeBrowserRegion = this._genomeBrowserRegion || this.clinicalAnalysis?.interpretation?.primaryFindings?.[0] || ""; - const genomeBrowserTracks = [ - { - type: "gene-overview", - overview: true, - config: {}, - }, - { - type: "sequence", - config: {}, - }, - { - type: "gene", - config: {}, - }, - { - type: "opencga-variant", - config: { - title: "Variants", - query: { - sample: this.clinicalAnalysis.proband.samples.map(s => s.id).join(","), - }, - }, - }, - ...(this.clinicalAnalysis.proband?.samples || []).map(sample => ({ - type: "opencga-alignment", - config: { - title: `Alignments - ${sample.id}`, - sample: sample.id, - }, - })), - ]; - const genomeBrowserConfig = { - cellBaseClient: this.cellbaseClient, - featuresOfInterest: [], - }; - - // Add interpretation panels to features of interest - if (this.clinicalAnalysis?.interpretation?.panels?.length > 0) { - genomeBrowserConfig.featuresOfInterest.push({ - name: "Panels of the interpretation", - category: true, - }); - - const colors = ["green", "blue", "darkorange", "blueviolet", "sienna", "indigo", "salmon"]; - const assembly = this.opencgaSession.project.organism?.assembly; - this.clinicalAnalysis.interpretation.panels.forEach((panel, index) => { - genomeBrowserConfig.featuresOfInterest.push({ - name: panel.name, - features: panel.genes - .map(gene => { - const coordinates = gene?.coordinates?.find(c => c.assembly === assembly); - if (!coordinates) { - return null; - } else { - const region = new Region(coordinates.location); - return { - chromosome: region.chromosome, - start: region.start, - end: region.end, - name: ` -
    ${gene.name}
    -
    ${region.toString()}
    - `, - }; - } - }) - .filter(gene => !!gene) - .sort((a, b) => a.name < b.name ? -1 : +1), - display: { - visible: true, - color: colors[index % colors.length], - }, - }); - }); - } - - if (this.clinicalAnalysis.interpretation?.primaryFindings.length > 0) { - if (genomeBrowserConfig.featuresOfInterest.length > 0) { - genomeBrowserConfig.featuresOfInterest.push({separator: true}); - } - genomeBrowserConfig.featuresOfInterest.push({ - name: "Variants", - category: true, - }); - genomeBrowserConfig.featuresOfInterest.push({ - name: "Primary Findings", - features: this.clinicalAnalysis.interpretation.primaryFindings.map(feature => { - const genes = Array.from(new Set(feature.annotation.consequenceTypes.filter(ct => !!ct.geneName).map(ct => ct.geneName))); - return { - id: feature.id, - chromosome: feature.chromosome, - start: feature.start, - end: feature.end ?? (feature.start + 1), - name: ` -
    -
    ${feature.id} (${feature.type})
    - ${feature.annotation.displayConsequenceType ? ` -
    - ${feature.annotation.displayConsequenceType} -
    - ` : ""} - ${genes.length > 0 ? ` -
    ${genes.join(", ")}
    - ` : ""} -
    - `, - }; - }), - display: { - visible: true, - color: "red", - }, - }); - } - if (type === "SINGLE" || type === "FAMILY") { items.push({ id: "variant-browser", From 2a5f617844558464907a66d5e6a059f9e82c3158 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:55:47 +0100 Subject: [PATCH 127/153] wc: remove useless variant generating genome browser config in interpreter browser template #TASK-5636 --- .../interpretation/variant-interpreter-browser-template.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index abeac5222a..99f36369ab 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -570,9 +570,6 @@ class VariantInterpreterBrowserTemplate extends LitElement { // Check for opencgaSession and clinicalAnalysis defined if (this.opencgaSession && this.clinicalAnalysis) { - const type = this.clinicalAnalysis.type.toUpperCase(); - - // Append tracks genomeBrowserTracks = [ { type: "gene-overview", From 883029cc0233abbd529b6736be02bd7265ed5fc5 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:58:06 +0100 Subject: [PATCH 128/153] wc: remove unused callers declarations in rearrangement browser config #TASK-5636 --- ...riant-interpreter-browser-rearrangement.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js index ea18fb2937..4869395452 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js @@ -273,25 +273,6 @@ class VariantInterpreterBrowserRearrangement extends LitElement { {id: "fileData"}, ]; - // Prepare dynamic Variant Caller INFO filters - const callers = ["Caveman", "strelka", "Pindel", "ASCAT", "Canvas", "BRASS", "Manta", "TNhaplotyper2", "Pisces", "CRAFT"]; - const callerFilters = callers.map(caller => { - const callerId = caller.toLowerCase(); - return { - id: callerId, - title: caller + " Filters", - description: () => html` - File filters for ${this.callerToFile[callerId].name} - `, - visible: () => this.callerToFile && this.callerToFile[callerId], - params: { - fileId: `${this.callerToFile ? this.callerToFile[callerId]?.name : null}`, - }, - }; - }); - - // Generate Genome browser columns and tracks configurations - return { title: "Cancer Case Interpreter", icon: "fas fa-search", From 08dafb0be19f19ba817ba9f0b19a4be08fac06e7 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 12:59:03 +0100 Subject: [PATCH 129/153] wc: minor code improvements in rearrangement grid #TASK-5636 --- ...riant-interpreter-browser-rearrangement.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js index 4869395452..45f0c67678 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-rearrangement.js @@ -266,13 +266,6 @@ class VariantInterpreterBrowserRearrangement extends LitElement { } getDefaultConfig() { - const lockedFields = [ - {id: "sample"}, - {id: "sampleData"}, - {id: "file"}, - {id: "fileData"}, - ]; - return { title: "Cancer Case Interpreter", icon: "fas fa-search", @@ -285,9 +278,6 @@ class VariantInterpreterBrowserRearrangement extends LitElement { searchButtonText: "Search", activeFilters: { alias: { - // Example: - // "region": "Region", - // "gene": "Gene", "ct": "Consequence Types" }, complexFields: [ @@ -295,10 +285,15 @@ class VariantInterpreterBrowserRearrangement extends LitElement { {id: "fileData", separator: ","}, ], hiddenFields: [], - lockedFields: lockedFields, + lockedFields: [ + {id: "sample"}, + {id: "sampleData"}, + {id: "file"}, + {id: "fileData"}, + ], }, callers: [], - sections: [ // sections and subsections, structure and order is respected + sections: [ { title: "Genomic", collapsed: false, From 06077afc6c0edc3f4442059eae53aeb2a902e70e Mon Sep 17 00:00:00 2001 From: gpveronica Date: Mon, 19 Feb 2024 15:39:16 +0100 Subject: [PATCH 130/153] wc - Changes propagated to Clinical analysis --- .../clinical/clinical-analysis-browser.js | 1 + .../clinical/clinical-analysis-view.js | 245 +++++++++++------- 2 files changed, 153 insertions(+), 93 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-browser.js b/src/webcomponents/clinical/clinical-analysis-browser.js index aac30047f1..390688dd10 100644 --- a/src/webcomponents/clinical/clinical-analysis-browser.js +++ b/src/webcomponents/clinical/clinical-analysis-browser.js @@ -142,6 +142,7 @@ export default class ClinicalAnalysisBrowser extends LitElement { .active="${params.active}" @selectanalysis="${params.onSelectClinicalAnalysis}" @selectrow="${e => params.onClickRow(e, "clinicalAnalysis")}" + @rowUpdate="${e => params.onComponentUpdate(e, "clinicalAnalysis")}" @clinicalAnalysisUpdate="${e => params.onComponentUpdate(e, "clinicalAnalysis")}" @settingsUpdate="${() => this.onSettingsUpdate()}"> diff --git a/src/webcomponents/clinical/clinical-analysis-view.js b/src/webcomponents/clinical/clinical-analysis-view.js index bc8348f20b..b16b184ea8 100644 --- a/src/webcomponents/clinical/clinical-analysis-view.js +++ b/src/webcomponents/clinical/clinical-analysis-view.js @@ -20,6 +20,7 @@ import LitUtils from "../commons/utils/lit-utils"; import CatalogGridFormatter from "../commons/catalog-grid-formatter.js"; import "../commons/forms/data-form.js"; import "../commons/image-viewer.js"; +import BioinfoUtils from "../../core/bioinfo/bioinfo-utils"; export default class ClinicalAnalysisView extends LitElement { @@ -95,7 +96,8 @@ export default class ClinicalAnalysisView extends LitElement { id: "files", className: "" } - ] + ], + pdf: true, }; this._config = this.getDefaultConfig(); } @@ -105,6 +107,17 @@ export default class ClinicalAnalysisView extends LitElement { this.requestUpdate(); } + #priorityFormatter(id, data) { + switch (data.priority.rank) { + case 1: return "label label-warning"; + case 2: return "label label-primary"; + case 3: return "label label-info"; + case 4: return "label label-success"; + case 5: return "label label-default"; + default: return "label"; + } + } + update(changedProperties) { if (changedProperties.has("clinicalAnalysisId")) { this.clinicalAnalysisIdObserver(); @@ -112,8 +125,11 @@ export default class ClinicalAnalysisView extends LitElement { if (changedProperties.has("settings")) { this.settingsObserver(); } - if (changedProperties.has("displayConfig")) { - this.displayConfig = {...this.displayConfigDefault, ...this.displayConfig}; + if (changedProperties.has("displayConfig") || changedProperties.has("opencgaSession")) { + this.displayConfig = { + ...this.displayConfigDefault, + ...this.displayConfig + }; this._config = this.getDefaultConfig(); } super.update(changedProperties); @@ -163,6 +179,7 @@ export default class ClinicalAnalysisView extends LitElement { } render() { + debugger if (this.isLoading) { return html``; } @@ -234,7 +251,7 @@ export default class ClinicalAnalysisView extends LitElement { field: "proband.id", }, { - title: "Disorders", + title: "Disorder", type: "complex", display: { template: "${disorder}", @@ -254,45 +271,38 @@ export default class ClinicalAnalysisView extends LitElement { { title: "Flags", field: "flags", - type: "custom", + type: "list", display: { visible: !this._config?.hiddenFields?.includes("flags"), - render: flags => html` - ${flags.map(flag => html` - ${flag?.id || "-"} - `)} - `, + separator: " ", + contentLayout: "horizontal", + template: "${id}", + className: { + "id": "badge badge-secondary", + }, } }, { title: "Status", - field: "status.name", + field: "status.id", + }, + { + title: "Priority", + type: "complex", display: { - visible: !this._config?.hiddenFields?.includes("status.name") && !!this.opencgaSession?.study?.configuration?.clinical?.status, - }, + template: "${priority.id}", + className: { + "priority.id": (id, data) => this.#priorityFormatter(id, data), + }, + } }, { title: "Description", field: "description", display: { - visible: !this._config?.hiddenFields?.includes("description"), errorMessage: "-", }, }, - { - title: "Priority", - field: "priority", - type: "custom", - display: { - visible: !this._config?.hiddenFields?.includes("priority") && !!this.opencgaSession?.study?.configuration?.clinical?.priorities, - render: priority => { - const priorityRankToColor = ["label-danger", "label-warning", "label-primary", "label-info", "label-success", "label-default"]; - return html` - ${priority.id} - `; - }, - }, - }, { title: "Assigned To", field: "analysts", @@ -300,25 +310,21 @@ export default class ClinicalAnalysisView extends LitElement { display: { contentLayout: "bullets", visible: !this._config?.hiddenFields?.includes("analyst.assignee") && !this._config?.hiddenFields?.includes("analyst.id"), - render: analyst => analyst.id, + format: analyst => analyst.id, }, }, { - title: "Creation date", + title: "Creation Date", field: "creationDate", - type: "custom", display: { - visible: !this._config?.hiddenFields?.includes("creationDate"), - render: creationDate => html`${moment(creationDate, "YYYYMMDDHHmmss").format("D MMM YY")}`, + format: date => UtilsNew.dateFormatter(date) }, }, { title: "Due date", field: "dueDate", - type: "custom", display: { - visible: !this._config?.hiddenFields?.includes("dueDate"), - render: dueDate => html`${moment(dueDate, "YYYYMMDDHHmmss").format("D MMM YY")}`, + format: date => UtilsNew.dateFormatter(date) }, } ] @@ -337,87 +343,130 @@ export default class ClinicalAnalysisView extends LitElement { }, { title: "Sex (Karyotypic)", - type: "custom", field: "proband", display: { - render: proband => ` - ${proband?.sex?.id ?? proband?.sex ?? "Not specified"} (${proband?.karyotypicSex ?? "Not specified"}) - `, + defaultValue: "Not specified", + format: proband => `${proband?.sex?.id ?? proband?.sex} (${proband?.karyotypicSex})` }, }, { title: "Date of Birth", - type: "complex", - display: { - template: "${proband.dateOfBirth} (${proband.lifeStatus})", - }, + field: "dateOfBirth", + }, + { + title: "Life Status", + field: "lifeStatus", + }, { title: "Disorders", field: "proband.disorders", type: "list", display: { + defaultValue: "N/A", contentLayout: "bullets", - render: disorder => { - let id = disorder.id; - if (disorder.id.startsWith("OMIM:")) { - id = html`
    ${disorder.id}`; - } - return html`${disorder.name} (${id})`; + transform: disorders => (disorders || []).map(disorder => ({disorder})), + template: "${disorder.name} (${disorder.id})", + link: { + "disorder.id": id => id.startsWith("OMIM:") ? + BioinfoUtils.getOmimOntologyLink(id) : + "", }, - defaultValue: "N/A", }, }, { title: "Phenotypes", field: "proband.phenotypes", - type: "custom", + type: "list", display: { - render: phenotypes => { - return (phenotypes || []) - .sort(item => item?.status === "OBSERVED" ? -1 : 1) - .map(phenotype => { - if (phenotype?.source && phenotype?.source?.toUpperCase() === "HPO") { - const url = `https://hpo.jax.org/app/browse/term/${phenotype.id}`; - return html` -
  • ${phenotype.name} (${phenotype.id}) - ${phenotype.status}
  • - `; - } else { - return html` -
  • ${phenotype.id} - ${phenotype.status}
  • - `; - } - }); - }, defaultValue: "N/A", + contentLayout: "bullets", + transform: phenotypes => (phenotypes || []) + .sort(item => item?.status === "OBSERVED" ? -1 : 1) + .map(phenotype => ({phenotype})), + template: "${phenotype.name} (${phenotype.id}) - Status: ${phenotype.status}", + link: { + "phenotype.id": id => id.startsWith("HP:") ? BioinfoUtils.getHpoLink(id) : id, + } + // render: phenotypes => { + // return (phenotypes || []) + // .sort(item => item?.status === "OBSERVED" ? -1 : 1) + // .map(phenotype => { + // if (phenotype?.source && phenotype?.source?.toUpperCase() === "HPO") { + // const url = `https://hpo.jax.org/app/browse/term/${phenotype.id}`; + // return html` + //
  • ${phenotype.name} (${phenotype.id}) - ${phenotype.status}
  • + // `; + // } else { + // return html` + //
  • ${phenotype.id} - ${phenotype.status}
  • + // `; + // } + // }); + // }, }, }, { title: "Samples", field: "proband.samples", type: "table", + style: { + "margin-top": "1em", + }, display: { + // defaultValue: "No sample found", defaultLayout: "vertical", + headerStyle: { + background: "#f5f5f5", + lineHeight: "0.5" + }, columns: [ + // { + // title: "ID", + // field: "id", + // formatter: (sampleId, sample) => { + // let somaticHtml = ""; + // if (typeof sample.somatic !== "undefined") { + // somaticHtml = sample.somatic ? "Somatic" : "Germline"; + // } + // return ` + //
    + // ${sampleId} + // ${somaticHtml ? `${somaticHtml}` : nothing} + //
    + // `; + // }, + // }, { title: "ID", - field: "id", - formatter: (sampleId, sample) => { - let somaticHtml = ""; - if (typeof sample.somatic !== "undefined") { - somaticHtml = sample.somatic ? "Somatic" : "Germline"; + type: "complex", + display: { + defaultValue: "-", + template: "${id} ${somatic}", + format: { + "somatic": (somatic, sample) => sample.somatic ? "Somatic" : "Germline", + }, + className: { + "somatic": "help-block" + }, + style: { + "id": { + "font-weight": "bold" + }, + "somatic": { + "margin": "5px 0" + }, } - return ` -
    - ${sampleId} - ${somaticHtml ? `${somaticHtml}` : nothing} -
    - `; }, }, { title: "Files", field: "fileIds", + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + }, }, { title: "Collection Method", @@ -429,21 +478,25 @@ export default class ClinicalAnalysisView extends LitElement { { title: "Preparation Method", field: "processing.preparationMethod", - formatter: (value, row) => value ?? "-" + display: { + defaultValue: "-", + }, }, { title: "Creation Date", field: "creationDate", - type: "custom", // this is not needed. feels right though - formatter: value => `${moment(value, "YYYYMMDDHHmmss").format("D MMM YY")}` + display: { + format: creationDate => UtilsNew.dateFormatter(creationDate, "D MMM YYYY, h:mm:ss a"), + } }, { title: "Status", field: "status.name", - formatter: (value, row) => value ?? "-" + display: { + defaultValue: "-", + }, }, ], - defaultValue: "No sample found", }, }, ] @@ -491,17 +544,23 @@ export default class ClinicalAnalysisView extends LitElement { errorMessage: "No family selected", }, }, + // { + // title: "Pedigree", + // type: "custom", + // display: { + // render: clinicalAnalysis => html` + // + // + // `, + // }, + // }, { title: "Pedigree", - type: "custom", - display: { - render: clinicalAnalysis => html` - - - `, - }, - } + type: "image", + field: "family.pedigreeGraph.base64", + }, + ] }, { From 291cfddf40bbf986ad80224df3c3cb96b517ec3d Mon Sep 17 00:00:00 2001 From: gpveronica Date: Mon, 19 Feb 2024 15:42:13 +0100 Subject: [PATCH 131/153] wc - Display pdf false Signed-off-by: gpveronica --- src/webcomponents/clinical/clinical-analysis-view.js | 3 +-- src/webcomponents/cohort/cohort-view.js | 2 +- src/webcomponents/disease-panel/disease-panel-summary.js | 2 +- src/webcomponents/file/file-view.js | 2 +- src/webcomponents/individual/individual-view.js | 2 +- src/webcomponents/job/job-view.js | 2 +- src/webcomponents/sample/sample-view.js | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-view.js b/src/webcomponents/clinical/clinical-analysis-view.js index b16b184ea8..4a89ab5760 100644 --- a/src/webcomponents/clinical/clinical-analysis-view.js +++ b/src/webcomponents/clinical/clinical-analysis-view.js @@ -97,7 +97,7 @@ export default class ClinicalAnalysisView extends LitElement { className: "" } ], - pdf: true, + pdf: false, }; this._config = this.getDefaultConfig(); } @@ -179,7 +179,6 @@ export default class ClinicalAnalysisView extends LitElement { } render() { - debugger if (this.isLoading) { return html``; } diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index 0d877beb8d..eae01dc606 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -67,7 +67,7 @@ export default class CohortView extends LitElement { titleVisible: false, titleWidth: 2, defaultValue: "-", - pdf: true + pdf: false, }; this._config = this.getDefaultConfig(); } diff --git a/src/webcomponents/disease-panel/disease-panel-summary.js b/src/webcomponents/disease-panel/disease-panel-summary.js index bba3ad93c1..998572e46a 100644 --- a/src/webcomponents/disease-panel/disease-panel-summary.js +++ b/src/webcomponents/disease-panel/disease-panel-summary.js @@ -66,7 +66,7 @@ export default class DiseasePanelSummary extends LitElement { buttonsVisible: false, showTitle: false, labelWidth: 3, - pdf: true, + pdf: false, }; this._config = this.getDefaultConfig(); } diff --git a/src/webcomponents/file/file-view.js b/src/webcomponents/file/file-view.js index 3ab78e55cd..e2272b2fe3 100644 --- a/src/webcomponents/file/file-view.js +++ b/src/webcomponents/file/file-view.js @@ -71,7 +71,7 @@ export default class FileView extends LitElement { titleVisible: false, titleWidth: 2, defaultValue: "-", - pdf: true, + pdf: false, }; this._config = this.getDefaultConfig(); } diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index 6d2a626716..012e3cfc9b 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -67,7 +67,7 @@ export default class IndividualView extends LitElement { defaultValue: "-", defaultLayout: "horizontal", buttonsVisible: false, - pdf: true, + pdf: false, }; this._config = this.getDefaultConfig(); } diff --git a/src/webcomponents/job/job-view.js b/src/webcomponents/job/job-view.js index 6cff3a4bca..62c1316416 100644 --- a/src/webcomponents/job/job-view.js +++ b/src/webcomponents/job/job-view.js @@ -68,7 +68,7 @@ export default class JobView extends LitElement { defaultValue: "-", defaultLayout: "horizontal", buttonsVisible: false, - pdf: true, + pdf: false, }; this._config = this.getDefaultConfig(); } diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index 335371ad24..f9b0c54839 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -67,7 +67,7 @@ export default class SampleView extends LitElement { titleVisible: false, titleWidth: 2, defaultValue: "-", - pdf: true, + pdf: false, }; this._config = this.getDefaultConfig(); } From b48c71729fe2ccfa2ff284c4f9ee88cfbbf994a4 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 17:00:20 +0100 Subject: [PATCH 132/153] wc: minor code style fixes in interpreter browser template #TASK-5636 --- .../interpretation/variant-interpreter-browser-template.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 99f36369ab..9a6ef327ac 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -276,7 +276,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { this.requestUpdate(); } - onResetVariants(e) { + onResetVariants() { this.clinicalAnalysisManager.reset(); this.preparedQuery = {...this.preparedQuery}; @@ -552,7 +552,6 @@ class VariantInterpreterBrowserTemplate extends LitElement { .tracks="${this._config.genomeBrowser.tracks}" .active="${this.active && this.activeView === "genome-browser"}"> - `} From 7b48b70f859ed6b0e217bb80cfa14017c8e3d600 Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 17:01:24 +0100 Subject: [PATCH 133/153] wc: remove configuration option to hide genome browser link #TASK-5636 --- .../interpretation/variant-interpreter-review-primary.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js b/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js index 02afba6744..c99a2fe4f2 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js @@ -135,7 +135,6 @@ export default class VariantInterpreterReviewPrimary extends LitElement { this._config.result.grid = { ...this._config.result.grid, ...this.opencgaSession.user.configs.IVA.settings[this.toolId].grid, - showGenomeBrowserLink: false, }; } @@ -357,7 +356,6 @@ export default class VariantInterpreterReviewPrimary extends LitElement { detailView: true, showReview: true, showActions: true, - showGenomeBrowserLink: false, showSelectCheckbox: true, multiSelection: false, From 91d28797fefcebbd8dd1ae51bb2d827c7565a9ee Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 19 Feb 2024 17:06:31 +0100 Subject: [PATCH 134/153] wc: use hideGenomeBrowser parameter in settings to hide the GB view #TASK-5636 --- .../variant-interpreter-browser-template.js | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js index 9a6ef327ac..45e87104a1 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-browser-template.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {html, LitElement} from "lit"; +import {html, LitElement, nothing} from "lit"; import VariantUtils from "../variant-utils.js"; import ClinicalAnalysisManager from "../../clinical/clinical-analysis-manager.js"; import LitUtils from "../../commons/utils/lit-utils.js"; @@ -178,11 +178,6 @@ class VariantInterpreterBrowserTemplate extends LitElement { this._config.filter.result.grid.highlights = this.settings.table.highlights; } - // Check to hide the genome browser link - if (this.settings?.hideGenomeBrowser) { - this._config.filter.result.grid.showGenomeBrowserLink = false; - } - // Add copy.execute functions if (this._config.filter.result.grid?.copies?.length > 0) { for (const copy of this._config.filter.result.grid?.copies) { @@ -463,7 +458,7 @@ class VariantInterpreterBrowserTemplate extends LitElement { -
    - ${!this._config.filter.result.grid.isRearrangement ? html` - - - ` : html` - - - `} -
    + ${!this.settings?.hideGenomeBrowser ? html` +
    + ${!this._config.filter.result.grid.isRearrangement ? html` + + + ` : html` + + + `} +
    + ` : nothing} From e84af23a12f2bb69408441ed6b1ef89798810927 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Tue, 20 Feb 2024 15:44:21 +0100 Subject: [PATCH 135/153] wc - Bug fixed --- src/webcomponents/family/opencga-family-relatedness-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/family/opencga-family-relatedness-view.js b/src/webcomponents/family/opencga-family-relatedness-view.js index 9c8dca3038..9b3ca21d5d 100644 --- a/src/webcomponents/family/opencga-family-relatedness-view.js +++ b/src/webcomponents/family/opencga-family-relatedness-view.js @@ -174,9 +174,9 @@ export default class OpencgaFamilyRelatednessView extends LitElement { } render() { - if (this.family.qualityControl?.relatedness?.length === 0) { + if (!this.family?.qualityControl?.relatedness?.length) { return html` -
    No QC data are available yet.
    +
    No Relatedness data are available yet.
    `; } From c2bad6e7f10363d6237220b27d24bd78cc9b19df Mon Sep 17 00:00:00 2001 From: gpveronica Date: Tue, 20 Feb 2024 15:44:53 +0100 Subject: [PATCH 136/153] wc - Some interpreter views migrated Signed-off-by: gpveronica --- .../file/qc/file-qc-ascat-metrics.js | 2 - .../sample/sample-variant-stats-view.js | 157 ++++++++++-------- .../variant-interpreter-qc-summary.js | 80 ++++----- 3 files changed, 118 insertions(+), 121 deletions(-) diff --git a/src/webcomponents/file/qc/file-qc-ascat-metrics.js b/src/webcomponents/file/qc/file-qc-ascat-metrics.js index 1d186d788a..56acc980b1 100644 --- a/src/webcomponents/file/qc/file-qc-ascat-metrics.js +++ b/src/webcomponents/file/qc/file-qc-ascat-metrics.js @@ -92,7 +92,6 @@ export default class FileQcAscatMetrics extends LitElement {
    No Ascat metrics provided.
    `; } - return html`

    ASCAT Metrics

    @@ -144,7 +143,6 @@ export default class FileQcAscatMetrics extends LitElement { { title: "ASCAT File", field: "file", - formatter: value => `${value || ""}`, }, { title: "ASCAT Aberrant Fraction", diff --git a/src/webcomponents/sample/sample-variant-stats-view.js b/src/webcomponents/sample/sample-variant-stats-view.js index 5cbf2e2196..ac379cb1fa 100644 --- a/src/webcomponents/sample/sample-variant-stats-view.js +++ b/src/webcomponents/sample/sample-variant-stats-view.js @@ -156,6 +156,11 @@ class SampleVariantStatsView extends LitElement { if (this.variantStats?.stats?.chromosomeCount) { this.variantStats.stats.chromosomeCount = UtilsNew.objectKeySort(this.variantStats.stats.chromosomeCount, CHROMOSOMES, false); } + if (this.variantStats?.query && typeof this.variantStats?.query === "object") { + this.variantStats.query = Object.entries(this.variantStats?.query) + .filter(([k, v]) => k !== "study") + .map(([key, value]) => ({key, value})); + } } else { this.statsSelect = []; this.variantStats = null; @@ -184,6 +189,43 @@ class SampleVariantStatsView extends LitElement { this.variantStats = this.sample.qualityControl[this._variantStatsPath].variantStats.find(stat => stat.id === e.detail.value); } + render() { + if (!this.variantStats?.stats?.id) { + return html` +
    + No Variant Stats found. +
    `; + } + + return html` + ${this.sample ? html` +
    +
    +
    + +
    + + +
    +
    +
    +
    + ` : null + } + +
    + + +
    + `; + } + getDefaultConfig() { return { title: "Summary", @@ -206,30 +248,30 @@ class SampleVariantStatsView extends LitElement { name: "Sample ID", field: "stats.id", display: { - style: "font-weight: bold" - } + style: { + "font-weight": "bold", + }, + }, }, { name: "Number of Variants", - field: "stats.variantCount", - type: "custom", + type: "complex", display: { - render: variantCount => { - if (variantCount > 0) { - return html`${variantCount} variants`; - } else { - return html`${variantCount} variants`; - } - } - } + template: "${stats.variantCount} variants", + style: { + "stats.variantCount": { + "color": variantCount => (variantCount < 0) ? "red" : "black", + }, + }, + }, }, { name: "Ti/Tv Ratio", field: "stats.tiTvRatio", display: { - decimals: 4, + format: value => value.toFixed(4), visible: tiTvRatio => tiTvRatio !== 0 - } + }, }, { name: "Quality Avg (Quality Standard Dev.)", @@ -237,41 +279,38 @@ class SampleVariantStatsView extends LitElement { display: { template: "${qualityAvg} (${qualityStdDev})", visible: variantStats => variantStats?.stats?.qualityAvg !== 0 - } + }, }, { name: "Heterozygosity Rate", field: "stats.heterozygosityRate", display: { - decimals: 4, + format: value => value.toFixed(4), visible: heterozygosityRate => heterozygosityRate !== 0 - } + }, }, { name: "Stats Query Filters", field: "query", - type: "custom", + type: "list", display: { - render: query => query && !UtilsNew.isEmpty(query) ? - Object.entries(query) - .map(([k, v]) => { - if (k !== "study") { - return html`${k}: ${v}
    `; - } else { - if (Object.keys(query).length === 1) { - return html`-`; - } - } - }) : - "none" - } + defaultValue: "None", + contentLayout: "vertical", + template: "${key}: ${value}", + style: { + key: { + "font-weight": "bold" + } + }, + }, }, { name: "Description", field: "description" } ] - }, { + }, + { title: "Variant Stats", display: { visible: variantStats => variantStats?.stats?.variantCount > 0 @@ -287,10 +326,20 @@ class SampleVariantStatsView extends LitElement { return html`
    - + +
    - + +
    `; @@ -394,7 +443,6 @@ class SampleVariantStatsView extends LitElement { ] }, { - // title: "plots2", display: { visible: variantStats => variantStats?.stats?.variantCount > 0 }, @@ -453,7 +501,8 @@ class SampleVariantStatsView extends LitElement { } } ] - }, { + }, + { title: "Variant Stats", display: { visible: variantStats => variantStats?.stats?.variantCount === 0 @@ -461,10 +510,8 @@ class SampleVariantStatsView extends LitElement { elements: [ { name: "Warning", - type: "custom", - display: { - render: () => html`No variants found` - } + type: "text", + text: "No variants found", } ] } @@ -472,36 +519,6 @@ class SampleVariantStatsView extends LitElement { }; } - render() { - if (!this.variantStats?.stats?.id) { - return html` -
    - No Variant Stats found. -
    `; - } - - return html` - ${this.sample ? - html` -
    -
    -
    - -
    - -
    -
    -
    -
    ` : - null - } - -
    - -
    - `; - } - } customElements.define("sample-variant-stats-view", SampleVariantStatsView); diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-qc-summary.js b/src/webcomponents/variant/interpretation/variant-interpreter-qc-summary.js index e2af840663..764f440057 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-qc-summary.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-qc-summary.js @@ -25,7 +25,7 @@ class VariantInterpreterQcSummary extends LitElement { super(); // Set status and init private properties - this._init(); + this.#init(); } createRenderRoot() { @@ -49,18 +49,12 @@ class VariantInterpreterQcSummary extends LitElement { }; } - _init() { + #init() { this._prefix = UtilsNew.randomString(8); this._config = this.getDefaultConfig(); } - connectedCallback() { - super.connectedCallback(); - - this._config = {...this.getDefaultConfig(), ...this.config}; - } - updated(changedProperties) { if (changedProperties.has("clinicalAnalysis")) { this.clinicalAnalysisObserver(); @@ -71,7 +65,10 @@ class VariantInterpreterQcSummary extends LitElement { } if (changedProperties.has("config")) { - this._config = {...this.getDefaultConfig(), ...this.config}; + this._config = { + ...this.getDefaultConfig(), + ...this.config + }; } } @@ -143,7 +140,11 @@ class VariantInterpreterQcSummary extends LitElement { // Alignment stats are the same for FAMILY and CANCER analysis return html`
    - + + +
    `; } @@ -171,18 +172,25 @@ class VariantInterpreterQcSummary extends LitElement { }, { title: "Proband", - field: "proband.id", - type: "custom", + type: "complex", display: { - render: probandId => html`${probandId}`, + template: "${proband.id}", + style: { + "proband.id": { + "font-weight": "bold", + } + } }, }, { title: "Disorder", - field: "disorder", - type: "custom", + type: "complex", display: { - render: disorder => UtilsNew.renderHTML(CatalogGridFormatter.disorderFormatter([disorder])), + template: "${disorder}", + format: { + disorder: disorder => CatalogGridFormatter.disorderFormatter([disorder]), + }, + defaultValue: "N/A", }, }, { @@ -207,56 +215,30 @@ class VariantInterpreterQcSummary extends LitElement { { title: "BAM File", field: "file", - formatter: value => ` -
    - - ${value} - -
    - `, }, + { title: "SD insert size", field: "sdInsertSize", - formatter: value => ` -
    - ${value} -
    - `, }, { title: "Average insert size", field: "avgInsertSize", - formatter: value => ` -
    - ${value} -
    - `, }, { title: "Duplicate read rate", field: "duplicateReadRate", - formatter: value => ` -
    - ${value} -
    - `, }, { title: "Average sequence depth", field: "avgSequenceDepth", - formatter: value => ` -
    - ${value} -
    - `, - } - ] - } + }, + ], + }, }, - ] - } - ] + ], + }, + ], }; } From 8a9f737adfc3b1aa8337a945b8f9c4dd2b46dfdf Mon Sep 17 00:00:00 2001 From: Josemi Date: Tue, 20 Feb 2024 17:02:36 +0100 Subject: [PATCH 137/153] wc: minor fixes in detail sections #TASK-5636 --- .../interpretation/variant-interpreter-detail.js | 16 ++++++++-------- .../variant-interpreter-review-primary.js | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-detail.js b/src/webcomponents/variant/interpretation/variant-interpreter-detail.js index c77549ec91..21ca6b8372 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-detail.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-detail.js @@ -148,7 +148,7 @@ export default class VariantInterpreterDetail extends LitElement { active: true, render: variant => html` @@ -160,7 +160,7 @@ export default class VariantInterpreterDetail extends LitElement { name: "Consequence Type", render: (variant, active) => html` `, @@ -170,7 +170,7 @@ export default class VariantInterpreterDetail extends LitElement { name: "Population Frequencies", render: (variant, active) => html` `, @@ -180,8 +180,8 @@ export default class VariantInterpreterDetail extends LitElement { name: "Clinical", render: variant => html` + .traitAssociation="${variant?.annotation?.traitAssociation}" + .geneTraitAssociation="${variant?.annotation?.geneTraitAssociation}"> `, }, @@ -202,7 +202,7 @@ export default class VariantInterpreterDetail extends LitElement { render: (variant, active, opencgaSession) => html` `, @@ -213,7 +213,7 @@ export default class VariantInterpreterDetail extends LitElement { render: (variant, active, opencgaSession) => html` `, @@ -234,7 +234,7 @@ export default class VariantInterpreterDetail extends LitElement { name: "Beacon", render: (variant, active, opencgaSession) => html` diff --git a/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js b/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js index c99a2fe4f2..7cb0dab8bc 100644 --- a/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js +++ b/src/webcomponents/variant/interpretation/variant-interpreter-review-primary.js @@ -383,7 +383,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { active: true, render: variant => html` @@ -395,7 +395,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { name: "Consequence Type", render: (variant, active) => html` `, @@ -405,7 +405,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { name: "Population Frequencies", render: (variant, active) => html` `, @@ -415,8 +415,8 @@ export default class VariantInterpreterReviewPrimary extends LitElement { name: "Clinical", render: variant => html` + .traitAssociation="${variant?.annotation?.traitAssociation}" + .geneTraitAssociation="${variant?.annotation?.geneTraitAssociation}"> `, }, @@ -448,7 +448,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { render: (variant, active, opencgaSession) => html` `, @@ -458,7 +458,7 @@ export default class VariantInterpreterReviewPrimary extends LitElement { name: "Beacon", render: (variant, active, opencgaSession) => html` From 100be03d5f15b4cad405ed7659d1462700333bcc Mon Sep 17 00:00:00 2001 From: gpveronica Date: Tue, 20 Feb 2024 19:05:43 +0100 Subject: [PATCH 138/153] wc - Commented out pdf button Signed-off-by: gpveronica --- src/webcomponents/clinical/clinical-analysis-review.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/clinical/clinical-analysis-review.js b/src/webcomponents/clinical/clinical-analysis-review.js index d5acd88a33..71257f82bf 100644 --- a/src/webcomponents/clinical/clinical-analysis-review.js +++ b/src/webcomponents/clinical/clinical-analysis-review.js @@ -349,11 +349,15 @@ export default class ClinicalAnalysisReview extends LitElement { } return html` + + Date: Mon, 26 Feb 2024 15:05:04 +0100 Subject: [PATCH 139/153] wc - Fixed small issues found in testing --- .../clinical/clinical-analysis-view.js | 9 ++++---- src/webcomponents/cohort/cohort-view.js | 22 +++++++++---------- src/webcomponents/commons/forms/data-form.js | 4 ++-- .../disease-panel/disease-panel-summary.js | 2 +- src/webcomponents/family/family-view.js | 6 ++++- .../individual/individual-view.js | 3 --- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-view.js b/src/webcomponents/clinical/clinical-analysis-view.js index 4a89ab5760..d911ae444e 100644 --- a/src/webcomponents/clinical/clinical-analysis-view.js +++ b/src/webcomponents/clinical/clinical-analysis-view.js @@ -350,12 +350,11 @@ export default class ClinicalAnalysisView extends LitElement { }, { title: "Date of Birth", - field: "dateOfBirth", + field: "proband.dateOfBirth", }, { title: "Life Status", - field: "lifeStatus", - + field: "proband.lifeStatus", }, { title: "Disorders", @@ -378,12 +377,12 @@ export default class ClinicalAnalysisView extends LitElement { field: "proband.phenotypes", type: "list", display: { - defaultValue: "N/A", + defaultValue: "", contentLayout: "bullets", transform: phenotypes => (phenotypes || []) .sort(item => item?.status === "OBSERVED" ? -1 : 1) .map(phenotype => ({phenotype})), - template: "${phenotype.name} (${phenotype.id}) - Status: ${phenotype.status}", + template: "${phenotype.name} (${phenotype.id}) - ${phenotype.status}", link: { "phenotype.id": id => id.startsWith("HP:") ? BioinfoUtils.getHpoLink(id) : id, } diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index eae01dc606..aaf54a2491 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -22,6 +22,7 @@ import PdfBuilder, {stylePdf} from "../commons/forms/pdf-builder.js"; import "../commons/forms/data-form.js"; import "../loading-spinner.js"; import "../study/annotationset/annotation-set-view.js"; +import CatalogGridFormatter from "../commons/catalog-grid-formatter"; export default class CohortView extends LitElement { @@ -288,23 +289,22 @@ export default class CohortView extends LitElement { // sortable: true, }, { - id: "somatic", title: "Somatic", + type: "custom", field: "somatic", - // sortable: true, - // width: "*", - formatter: value => value ? "true" : "false", + display: { + render: somatic => somatic ? "true" : "false", + } }, { - id: "phenotypes", title: "Phenotypes", field: "phenotypes", - // sortable: true, - // width: "*", - formatter: (value, row) => { - return row?.phenotypes?.length > 0 ? row.phenotypes.map(d => d.id).join(", ") : "-"; - } - } + type: "list", + display: { + contentLayout: "bullets", + format: phenotype => CatalogGridFormatter.phenotypesFormatter([phenotype]), + }, + }, ], // pagination: true, // search: true, diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index f2e3fc70d6..23c1f7d38a 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -196,7 +196,7 @@ export default class DataForm extends LitElement { } if (element?.display?.link?.[match]) { const href = element?.display?.link?.[match](value, data); - value = `${value}`; + value = href ? `${value}` : value; } if (element?.display?.className?.[match] || element?.display?.style?.[match]) { const style = this._parseStyleField(element.display?.style?.[match], value, data); @@ -830,7 +830,7 @@ export default class DataForm extends LitElement { } _createTextElement(element) { - const value = typeof element.text === "function" ? element.text(this.data) : element.text; + const value = typeof element.text === "function" ? element.text(this.data, element.field) : element.text; const textClass = element.display?.textClassName ?? ""; const textStyle = element.display?.textStyle ?? ""; const notificationClass = element.type === "notification" ? DataForm.NOTIFICATION_TYPES[element?.display?.notificationType] || "alert alert-info" : ""; diff --git a/src/webcomponents/disease-panel/disease-panel-summary.js b/src/webcomponents/disease-panel/disease-panel-summary.js index 998572e46a..971dbb64b8 100644 --- a/src/webcomponents/disease-panel/disease-panel-summary.js +++ b/src/webcomponents/disease-panel/disease-panel-summary.js @@ -165,7 +165,7 @@ export default class DiseasePanelSummary extends LitElement { display: { template: "${id} (UUID: ${uuid})", link: { - id: (id, data) => BioinfoUtils.getPanelAppLink(data.source.id), + id: (id, data) => data?.source?.project === "PanelApp"? BioinfoUtils.getPanelAppLink(data.source.id) : false, }, } }, diff --git a/src/webcomponents/family/family-view.js b/src/webcomponents/family/family-view.js index fd0c654fc4..0bb865e519 100644 --- a/src/webcomponents/family/family-view.js +++ b/src/webcomponents/family/family-view.js @@ -325,7 +325,11 @@ export default class FamilyView extends LitElement { display: { format: phenotype => CatalogGridFormatter.phenotypesFormatter([phenotype]) } - } + }, + { + title: "Life Status", + field: "lifeStatus", + }, ] } }, diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index 012e3cfc9b..7ea7fae7a3 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -417,9 +417,6 @@ export default class IndividualView extends LitElement { // formatter: value => value ? "true" : "false", display: { render: somatic => somatic ? "true" : "false", - style: { - color: "red" - } } }, { From 647d11c4bf8a9213784d6c4814a7beda1cd2645e Mon Sep 17 00:00:00 2001 From: gpveronica Date: Mon, 26 Feb 2024 15:15:29 +0100 Subject: [PATCH 140/153] wc - Undo life status fix Signed-off-by: gpveronica --- src/webcomponents/family/family-view.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/webcomponents/family/family-view.js b/src/webcomponents/family/family-view.js index 0bb865e519..47d7c8b6a5 100644 --- a/src/webcomponents/family/family-view.js +++ b/src/webcomponents/family/family-view.js @@ -326,10 +326,6 @@ export default class FamilyView extends LitElement { format: phenotype => CatalogGridFormatter.phenotypesFormatter([phenotype]) } }, - { - title: "Life Status", - field: "lifeStatus", - }, ] } }, From fdbb827681d54cb26afeff206aaece617794c836 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 29 Feb 2024 16:36:50 +0100 Subject: [PATCH 141/153] wc - Migrated user-info defaultConfig to new data model Signed-off-by: gpveronica --- src/webcomponents/user/user-info.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/webcomponents/user/user-info.js b/src/webcomponents/user/user-info.js index fc8dcfd697..da8fc8d872 100644 --- a/src/webcomponents/user/user-info.js +++ b/src/webcomponents/user/user-info.js @@ -68,7 +68,9 @@ export default class UserInfo extends LitElement { { title: "Organization", field: "organization", - defaultValue: "-", + display: { + defaultValue: "-", + }, }, { title: "Account type", @@ -77,18 +79,19 @@ export default class UserInfo extends LitElement { { title: "Member since", field: "account.creationDate", - type: "custom", display: { - render: date => date ? UtilsNew.dateFormatter(date) : "Not provided", + defaultValue: "Not provided", + format: date => UtilsNew.dateFormatter(date) } }, { title: "Synced from", field: "account.authentication", - type: "custom", + // CAUTION 20240229 VERO: bug in visible function: + // the argument authentication is the entire data model, not the field account.authentication display: { visible: authentication => authentication.id === "internal", - render: authentication => authentication.id, + format: authentication => authentication.id, } } ] From 447e6c1b6495573ad7541b01f30c82c713dfbafe Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 29 Feb 2024 17:57:11 +0100 Subject: [PATCH 142/153] wc - Fixed defect for previewing user list of projects and studies Signed-off-by: gpveronica --- src/webcomponents/user/user-projects.js | 31 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/webcomponents/user/user-projects.js b/src/webcomponents/user/user-projects.js index 58b8f00340..5b2ec7f055 100644 --- a/src/webcomponents/user/user-projects.js +++ b/src/webcomponents/user/user-projects.js @@ -93,18 +93,18 @@ export default class UserProjects extends LitElement { }, { title: "Project ID", - text: project.id || "-", type: "text", + text: project.id, display: { - textStyle: "padding-left:16px;", + defaultValue: "-", }, }, { title: "Project Description", - text: project.description || "-", type: "text", + text: project.description, display: { - textStyle: "padding-left:16px;", + defaultValue: "-", }, }, { @@ -112,8 +112,8 @@ export default class UserProjects extends LitElement { text: project.attributes.release, type: "text", display: { + visible: !!project?.attributes?.release, textStyle: "padding-left:16px;", - visible: !!project?.attributes?.release }, }, { @@ -153,8 +153,13 @@ export default class UserProjects extends LitElement { { type: "table", // title: "Studies", - defaultValue: project.studies, display: { + defaultLayout: "vertical", + headerStyle: { + background: "#f5f5f5", + lineHeight: "0.5" + }, + getData: () => project.studies, columns: [ { title: "ID", @@ -167,17 +172,25 @@ export default class UserProjects extends LitElement { { title: "Description", field: "description", - formatter: value => UtilsNew.isNotEmpty(value) ? value : "-", + display: { + defaultValue: "-", + }, }, { title: "Creation", field: "creationDate", - formatter: value => UtilsNew.dateFormatter(value) ?? "-", + display: { + format: date => UtilsNew.dateFormatter(date), + }, }, { title: "FQN", field: "fqn", }, + // Caution 20240229 Vero: commented out because: + // (a) not working + // (b) further discussion needed to migrate to new config data model + /* { title: "Links", field: "id", @@ -187,8 +200,8 @@ export default class UserProjects extends LitElement { `, }, + */ ], - defaultLayout: "vertical", }, }, ], From 490ba01d554bcda536f963e45d7fedfc8c5801d7 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Thu, 29 Feb 2024 22:05:14 +0100 Subject: [PATCH 143/153] wc - Migrated table views into new config data model in clinical analysis create form Signed-off-by: gpveronica --- .../clinical/clinical-analysis-create.js | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-create.js b/src/webcomponents/clinical/clinical-analysis-create.js index 0043c2f764..49206f0280 100644 --- a/src/webcomponents/clinical/clinical-analysis-create.js +++ b/src/webcomponents/clinical/clinical-analysis-create.js @@ -28,6 +28,7 @@ import "../commons/image-viewer.js"; import "./filters/clinical-priority-filter.js"; import "./filters/clinical-flag-filter.js"; import "./filters/clinical-analyst-filter.js"; +import CatalogGridFormatter from "../commons/catalog-grid-formatter"; export default class ClinicalAnalysisCreate extends LitElement { @@ -592,24 +593,41 @@ export default class ClinicalAnalysisCreate extends LitElement { defaultValue: "No proband or sample selected.", columns: [ { - id: "id", title: "ID", - field: "id", - formatter: (value, row) => `${row.id}`, + type: "complex", + display: { + defaultValue: "-", + template: "${id} ${somatic}", + format: { + "somatic": (somatic, sample) => sample.somatic ? "Somatic" : "Germline", + }, + className: { + "somatic": "help-block" + }, + style: { + "id": { + "font-weight": "bold" + }, + "somatic": { + "margin": "5px 0" + }, + } + }, }, { - id: "fileIds", title: "Files", field: "fileIds", - formatter: values => { - return (values || []).filter(file => file?.includes(".vcf")).join("
    ") || "-"; + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + transform: values => (values || []).filter(file => file?.includes(".vcf")), }, }, { id: "Status", title: "Status", field: "internal.status.id", - formatter: value => value ?? "-" }, ] } @@ -691,32 +709,31 @@ export default class ClinicalAnalysisCreate extends LitElement { errorClassName: "", columns: [ { - id: "individualId", title: "Individual ID", - formatter: (value, row) => ` -
    - ${row.id} -
    -
    - ${row?.sex?.id || "Not specified"} (${row.karyotypicSex || "Not specified"}) -
    - `, + field: "id", + display: { + style: { + "font-weight": "bold" + } + } }, { - id: "name", - title: "Individual Name", - field: "name", + title: "Sex", + field: "sex", + display: { + format: sex => sex.id + } }, { id: "samples", title: "Samples", field: "samples", - formatter: values => { - if (!values || values.length === 0) { - return "-"; - } - return values.map(sample => `
    ${sample.id}
    `); - }, + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + template: "${id}" + } }, { id: "fatherId", @@ -729,22 +746,14 @@ export default class ClinicalAnalysisCreate extends LitElement { field: "mother.id", }, { - id: "disorders", title: "Disorders", field: "disorders", - formatter: (values, row) => { - if (values && values.length > 0) { - let id = values[0].id; - const name = values[0].name; - if (id?.startsWith("OMIM:")) { - id = `${id}`; - } - return `${name} (${id})`; - } else { - return "N/A"; - } + type: "list", + display: { + defaultValue: "N/A", + format: disorder => CatalogGridFormatter.disorderFormatter([disorder]) } - } + }, ] } }, @@ -841,15 +850,23 @@ export default class ClinicalAnalysisCreate extends LitElement { defaultValue: "No proband or sample(s) selected.", columns: [ { - id: "fileIds", title: "ID", - formatter: (value, row) => `${row.id}`, + field: "id", + display: { + style: { + "font-weight": "bold" + } + }, }, { - id: "fileIds", title: "Files", field: "fileIds", - formatter: (values, row) => `${values.join("\n")}`, + type: "list", + display: { + defaultValue: "-", + contentLayout: "vertical", + transform: values => (values || []).filter(file => file?.includes(".vcf")), + }, }, { title: "Somatic", @@ -858,7 +875,6 @@ export default class ClinicalAnalysisCreate extends LitElement { { title: "Status", field: "internal.status.id", - formatter: value => value ?? "-" } ] } From 982809e96f7e7d934bf7838c3d999c295f692035 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 1 Mar 2024 09:55:10 +0100 Subject: [PATCH 144/153] wc - Fixed family members, individual ID and sex info Signed-off-by: gpveronica --- .../clinical/clinical-analysis-create.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-create.js b/src/webcomponents/clinical/clinical-analysis-create.js index 49206f0280..b391bea67f 100644 --- a/src/webcomponents/clinical/clinical-analysis-create.js +++ b/src/webcomponents/clinical/clinical-analysis-create.js @@ -710,19 +710,25 @@ export default class ClinicalAnalysisCreate extends LitElement { columns: [ { title: "Individual ID", - field: "id", + type: "complex", display: { + defaultValue: "-", + template: "${id} ${sex}", + format: { + "sex": (sex, member) => `${sex?.id ?? sex}(${member.karyotypicSex})` + }, + className: { + "sex": "help-block" + }, style: { - "font-weight": "bold" + "id": { + "font-weight": "bold" + }, + "sex": { + "margin": "5px 0" + }, } - } - }, - { - title: "Sex", - field: "sex", - display: { - format: sex => sex.id - } + }, }, { id: "samples", From 250cd4e401572aa86bab90673ca95cff57f7bfde Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 1 Mar 2024 12:33:49 +0100 Subject: [PATCH 145/153] wc - PR request fixed. Removed somatic information from selected samples in Single Analysis Configuration Signed-off-by: gpveronica --- .../clinical/clinical-analysis-create.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-create.js b/src/webcomponents/clinical/clinical-analysis-create.js index b391bea67f..271cb1b4b2 100644 --- a/src/webcomponents/clinical/clinical-analysis-create.js +++ b/src/webcomponents/clinical/clinical-analysis-create.js @@ -594,23 +594,11 @@ export default class ClinicalAnalysisCreate extends LitElement { columns: [ { title: "ID", - type: "complex", + field: "id", display: { defaultValue: "-", - template: "${id} ${somatic}", - format: { - "somatic": (somatic, sample) => sample.somatic ? "Somatic" : "Germline", - }, - className: { - "somatic": "help-block" - }, style: { - "id": { - "font-weight": "bold" - }, - "somatic": { - "margin": "5px 0" - }, + "font-weight": "bold" } }, }, From 77ab105024c0b0c9cfce2faffc0f5f9aaa932e38 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 1 Mar 2024 12:44:47 +0100 Subject: [PATCH 146/153] wc - Minor change for leaving the columns aligned Signed-off-by: gpveronica --- src/webcomponents/user/user-projects.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/webcomponents/user/user-projects.js b/src/webcomponents/user/user-projects.js index 5b2ec7f055..537330d271 100644 --- a/src/webcomponents/user/user-projects.js +++ b/src/webcomponents/user/user-projects.js @@ -113,7 +113,6 @@ export default class UserProjects extends LitElement { type: "text", display: { visible: !!project?.attributes?.release, - textStyle: "padding-left:16px;", }, }, { @@ -121,24 +120,20 @@ export default class UserProjects extends LitElement { text: owner || "-", type: "text", display: { - textStyle: "padding-left:16px;font-weight:bold;", + "font-weight": "bold" }, }, { title: "Species", text: `${project.organism?.scientificName || "-"} (${project.organism?.assembly || "-"})`, type: "text", - display: { - textStyle: "padding-left:16px;", - }, + }, { title: "CellBase", text: `${project.cellbase?.url || "-"} (${project.cellbase?.version || "-"}, Data Release: ${project.cellbase?.dataRelease || "-"})`, type: "text", - display: { - textStyle: "padding-left:16px;", - }, + }, // Generate a table with all studies of this project of this user { From 20f1fe61503994dd680d86777126aa28db50b74c Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 1 Mar 2024 13:15:44 +0100 Subject: [PATCH 147/153] wc - Minor fix Signed-off-by: gpveronica --- src/webcomponents/user/user-projects.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webcomponents/user/user-projects.js b/src/webcomponents/user/user-projects.js index 537330d271..ebe0bd85ad 100644 --- a/src/webcomponents/user/user-projects.js +++ b/src/webcomponents/user/user-projects.js @@ -120,7 +120,9 @@ export default class UserProjects extends LitElement { text: owner || "-", type: "text", display: { - "font-weight": "bold" + style: { + "font-weight": "bold" + } }, }, { From cb648705deee6472e1c99504c772eb13f9b9b5f4 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Fri, 1 Mar 2024 13:19:40 +0100 Subject: [PATCH 148/153] wc - PR request resolved, removed extra lines Signed-off-by: gpveronica --- src/webcomponents/user/user-projects.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webcomponents/user/user-projects.js b/src/webcomponents/user/user-projects.js index ebe0bd85ad..8ad7d8a97c 100644 --- a/src/webcomponents/user/user-projects.js +++ b/src/webcomponents/user/user-projects.js @@ -129,13 +129,11 @@ export default class UserProjects extends LitElement { title: "Species", text: `${project.organism?.scientificName || "-"} (${project.organism?.assembly || "-"})`, type: "text", - }, { title: "CellBase", text: `${project.cellbase?.url || "-"} (${project.cellbase?.version || "-"}, Data Release: ${project.cellbase?.dataRelease || "-"})`, type: "text", - }, // Generate a table with all studies of this project of this user { From 3985eebf0d1f523a23299daae07827554f2d3570 Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 1 Mar 2024 15:37:40 +0100 Subject: [PATCH 149/153] wc: add elementWidth field to fix narrowed elements in data-form #TASK-5765 --- src/webcomponents/commons/forms/data-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 23c1f7d38a..3e97303b9c 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -311,7 +311,7 @@ export default class DataForm extends LitElement { } _getElementWidth(element, section) { - return element?.display?.width ?? section?.display?.width ?? this.config?.display?.width ?? null; + return element?.display?.width ?? section?.display?.elementWidth ?? this.config?.display?.elementWidth ?? null; } _getElementTitleWidth(element, section) { From 86c45406e070f112af7d64ad41abb98c40a844ed Mon Sep 17 00:00:00 2001 From: gpveronica Date: Mon, 4 Mar 2024 17:22:02 +0100 Subject: [PATCH 150/153] wc - Fixed valid value in data form --- src/webcomponents/commons/forms/data-form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webcomponents/commons/forms/data-form.js b/src/webcomponents/commons/forms/data-form.js index 23c1f7d38a..5ef1a851eb 100644 --- a/src/webcomponents/commons/forms/data-form.js +++ b/src/webcomponents/commons/forms/data-form.js @@ -132,7 +132,7 @@ export default class DataForm extends LitElement { } // If 'value' exists we must apply the functions, DO NOT change the order - if (value) { + if (value || typeof value === "boolean") { if (display?.format) { // Check if response is actually an HTML // value = UtilsNew.renderHTML(display.format(value)); From 576648ed66e5fc3e131fd7bd6d9b121a957069c3 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Mon, 4 Mar 2024 17:23:03 +0100 Subject: [PATCH 151/153] wc - Migrated boolean datapoints to new config data model Signed-off-by: gpveronica --- src/webcomponents/cohort/cohort-view.js | 4 ---- src/webcomponents/individual/individual-view.js | 5 ----- src/webcomponents/sample/sample-view.js | 3 --- 3 files changed, 12 deletions(-) diff --git a/src/webcomponents/cohort/cohort-view.js b/src/webcomponents/cohort/cohort-view.js index aaf54a2491..8a49ddc4e7 100644 --- a/src/webcomponents/cohort/cohort-view.js +++ b/src/webcomponents/cohort/cohort-view.js @@ -290,11 +290,7 @@ export default class CohortView extends LitElement { }, { title: "Somatic", - type: "custom", field: "somatic", - display: { - render: somatic => somatic ? "true" : "false", - } }, { title: "Phenotypes", diff --git a/src/webcomponents/individual/individual-view.js b/src/webcomponents/individual/individual-view.js index 7ea7fae7a3..15e3548db0 100644 --- a/src/webcomponents/individual/individual-view.js +++ b/src/webcomponents/individual/individual-view.js @@ -412,12 +412,7 @@ export default class IndividualView extends LitElement { }, { title: "Somatic", - type: "custom", field: "somatic", - // formatter: value => value ? "true" : "false", - display: { - render: somatic => somatic ? "true" : "false", - } }, { title: "Phenotypes", diff --git a/src/webcomponents/sample/sample-view.js b/src/webcomponents/sample/sample-view.js index f9b0c54839..f081202e4c 100644 --- a/src/webcomponents/sample/sample-view.js +++ b/src/webcomponents/sample/sample-view.js @@ -208,9 +208,6 @@ export default class SampleView extends LitElement { { title: "Somatic", field: "somatic", - display: { - defaultValue: "false", - }, }, { title: "Version", From 51aeadeb7f3919d17870f775dd1540eb555727c8 Mon Sep 17 00:00:00 2001 From: gpveronica Date: Tue, 5 Mar 2024 11:23:32 +0100 Subject: [PATCH 152/153] wc - Fixed error message in clinical analysis view empty list of phenotypes Signed-off-by: gpveronica --- src/webcomponents/clinical/clinical-analysis-view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webcomponents/clinical/clinical-analysis-view.js b/src/webcomponents/clinical/clinical-analysis-view.js index d911ae444e..e3f22aef50 100644 --- a/src/webcomponents/clinical/clinical-analysis-view.js +++ b/src/webcomponents/clinical/clinical-analysis-view.js @@ -361,7 +361,7 @@ export default class ClinicalAnalysisView extends LitElement { field: "proband.disorders", type: "list", display: { - defaultValue: "N/A", + defaultValue: "-", contentLayout: "bullets", transform: disorders => (disorders || []).map(disorder => ({disorder})), template: "${disorder.name} (${disorder.id})", @@ -377,7 +377,7 @@ export default class ClinicalAnalysisView extends LitElement { field: "proband.phenotypes", type: "list", display: { - defaultValue: "", + defaultValue: "-", contentLayout: "bullets", transform: phenotypes => (phenotypes || []) .sort(item => item?.status === "OBSERVED" ? -1 : 1) From a529194176321d1e48c974d5f83cfde4bff9c33d Mon Sep 17 00:00:00 2001 From: Josemi Date: Fri, 8 Mar 2024 17:16:10 +0100 Subject: [PATCH 153/153] Prepare new release v2.12.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7b81582a4..478880079e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsorolla", - "version": "2.12.3-dev", + "version": "2.12.3", "description": "JSorolla is a JavaScript bioinformatic library for data analysis and genomic visualization", "repository": { "type": "git",