From 6b91861bbb40784b9b18e8c76975f1b29f29c96f Mon Sep 17 00:00:00 2001 From: imedina Date: Wed, 6 Mar 2024 02:32:09 +0000 Subject: [PATCH 1/7] Several improvements in Variant Download --- .../variant/variant-browser-grid.js | 1 - src/webcomponents/variant/variant-utils.js | 428 +++++++++--------- 2 files changed, 210 insertions(+), 219 deletions(-) diff --git a/src/webcomponents/variant/variant-browser-grid.js b/src/webcomponents/variant/variant-browser-grid.js index 5efa87051b..b8ec803194 100644 --- a/src/webcomponents/variant/variant-browser-grid.js +++ b/src/webcomponents/variant/variant-browser-grid.js @@ -1060,7 +1060,6 @@ export default class VariantBrowserGrid extends LitElement {
- `; } diff --git a/src/webcomponents/variant/variant-utils.js b/src/webcomponents/variant/variant-utils.js index f9a07f9767..e0e801f423 100644 --- a/src/webcomponents/variant/variant-utils.js +++ b/src/webcomponents/variant/variant-utils.js @@ -21,7 +21,7 @@ import VariantGridFormatter from "./variant-grid-formatter.js"; export default class VariantUtils { - static jsonToTabConvert(variants, studiesPopFrequencies, samples, nucleotideGenotype, fieldList) { + static jsonToTabConvert(variants, populationFrequenciesStudies, samples, nucleotideGenotype, fieldList) { const rows = []; let populationMap = {}; const headerString = []; @@ -29,34 +29,38 @@ export default class VariantUtils { // took from the first result. Is there a better way? // allele count / allele freqs - const cohortAlleleStatsColumns = []; - const alleleStats = []; - const studyIds = []; + // const cohortAlleleStatsColumns = []; + // const alleleStats = []; + // const studyIds = []; + + // variants?.[0]?.studies + // ?.filter(s => s.studyId.includes("@")) + // .map(s => s.studyId.split(":")[1]); // Code to Remove // ### - if (variants[0].studies?.length) { - variants[0].studies.forEach(study => { - if (study.studyId.includes("@")) { - const studyId = study.studyId.split(":")[1]; - studyIds.push(studyId); - cohortAlleleStatsColumns.push(`cohorts.${studyId}.alleleCount`, `cohorts.${studyId}.altAlleleFreq`); - // alleleCount, altAlleleFreq - - // cohort ALL is always the first element in study.stats - // Remove - alleleStats.push({ - id: studyId, - stats: study.stats, - }); - } else { - console.error("Unexpected studyId format"); - } - }); - } + // if (variants[0].studies?.length) { + // variants[0].studies.forEach(study => { + // if (study.studyId.includes("@")) { + // const studyId = study.studyId.split(":")[1]; + // studyIds.push(studyId); + // cohortAlleleStatsColumns.push(`cohorts.${studyId}.alleleCount`, `cohorts.${studyId}.altAlleleFreq`); + // // alleleCount, altAlleleFreq + // + // // cohort ALL is always the first element in study.stats + // // Remove + // alleleStats.push({ + // id: studyId, + // stats: study.stats, + // }); + // } else { + // console.error("Unexpected studyId format"); + // } + // }); + // } // ##### - const popStudyIds = studiesPopFrequencies?.map(study => "popfreq." + study.id); + // const popStudyIds = populationFrequenciesStudies?.map(popFreqStudy => "popfreq." + popFreqStudy.id); /* // explicit list gives less maintainability but we need customisation (also in some cases for each column there is more than 1 field) */ let flatFieldList = []; @@ -66,10 +70,13 @@ export default class VariantUtils { // default list flatFieldList = [ "id", + "snp_id", "gene", "type", + "hgvs", // Adding SAMPLES (includeSample=all in VB and Case samples in Sample VB) - ...samples.map(sample => sample.id || sample), + // ...samples.map(sample => sample.id || sample), + "samples", "consequenceType", "deleteriousness.SIFT", "deleteriousness.polyphen", @@ -82,15 +89,16 @@ export default class VariantUtils { // AC / AF // fieldList (columns in the grid) is in the form: cohorts.RD38, cohorts.CG38 // TSV in the form: cohort.RD38.alleleCount,cohort.RD38.altAlleleFreq - ...studyIds.map(studyId => `cohorts.${studyId}`), + // ...studyIds.map(studyId => `cohorts.${studyId}`), + "cohortStats", // fieldList (columns in the grid) is in the form: popfreq.1kG_phase3, popfreq.GNOMAD_GENOMES // TSV in the form: popfreq.1kG_phase3_SAS,popfreq.GNOMAD_GENOMES_ALL,popfreq.GNOMAD_GENOMES_AFR - ...popStudyIds, + // ...popStudyIds, + "populationFrequencies", "clinicalInfo.clinvar", "clinicalInfo.cosmic", + "acmgPrediction", ]; - - } else { flatFieldList = fieldList .filter(f => f.export && !f.excludeFromExport) @@ -99,116 +107,112 @@ export default class VariantUtils { // flatFieldList = fieldList.filter(f => f.export).flatMap(f => f.children?.filter(f => f.export).map(x => `${f.id}.${x.id}`) ?? f.id); } - flatFieldList.forEach(f => { - if ("id" === f) { - headerString.push("id"); - headerString.push("SNP_ID"); - } else if (f.startsWith("cohorts.")) { - // Cohorts Variant Browser - studyIds.forEach(id => { - if (f === "cohorts." + id) { - headerString.push(`cohorts.${id}.alleleCount`, `cohorts.${id}.altAlleleFreq`); - } - }); - } else if ("frequencies.cohort" === f) { - // Cohorts in Sample Variant Browser - studyIds.forEach(id => headerString.push(`cohorts.${id}.alleleCount`, `cohorts.${id}.altAlleleFreq`)); - } else if (f.startsWith("popfreq.")) { - // Pop freq in Variant Browser - studiesPopFrequencies.forEach(study => { - if (f === "popfreq." + study.id) { - headerString.push(...study.populations.map(pop => "popfreq." + study.id + "_" + pop.id)); - } - }); - } else if ("frequencies.populationFrequencies" === f) { - // Pop freq in Sample Variant Browser - studiesPopFrequencies.forEach(study => headerString.push(...study.populations.map(pop => "popfreq." + study.id + "_" + pop.id))); - } else { - headerString.push(f); - } - - }); + // flatFieldList.forEach(f => { + // if ("id" === f) { + // headerString.push("id"); + // // headerString.push("snp_id"); + // } else if (f.startsWith("cohorts.")) { + // // Cohorts Variant Browser + // studyIds.forEach(id => { + // if (f === "cohorts." + id) { + // headerString.push(`cohorts.${id}.alleleCount`, `cohorts.${id}.altAlleleFreq`); + // } + // }); + // } else if ("frequencies.cohort" === f) { + // // Cohorts in Sample Variant Browser + // studyIds.forEach(id => headerString.push(`cohorts.${id}.alleleCount`, `cohorts.${id}.altAlleleFreq`)); + // } else if (f.startsWith("popfreq.")) { + // // Pop freq in Variant Browser + // populationFrequenciesStudies.forEach(study => { + // if (f === "popfreq." + study.id) { + // headerString.push(...study.populations.map(pop => "popfreq." + study.id + "_" + pop.id)); + // } + // }); + // } else if ("frequencies.populationFrequencies" === f) { + // // Pop freq in Sample Variant Browser + // populationFrequenciesStudies.forEach(study => headerString.push(...study.populations.map(pop => "popfreq." + study.id + "_" + pop.id))); + // } else { + // headerString.push(f); + // } + // }); // TSV header - rows.push(headerString.join("\t")); + rows.push(flatFieldList.join("\t")); - for (const v of variants) { - const row = []; - let genes = new Set(); - let ct = new Set(); - let sift, polyphen; + for (const variant of variants) { + const genes = new Set(); + const consequenceTypeNames = new Set(); + const proteinSubstitutionScores = {sift: "-", polyphen: "-", revel: "-"}; let cadd = "-"; let phylop = "-"; let phastCons = "-"; let gerp = "-"; - // cohorts - // popfreqs let clinvar = new Set(); let cosmic = new Map(); - let prediction = "-"; + let acmgPrediction = "-"; populationMap = {}; const dataToTsv = {}; - const description = {sift: "-", polyphen: "-"}; - let min = 10; - let max = 0; - if (typeof v.annotation !== "undefined") { - if (typeof v.annotation.consequenceTypes !== "undefined" && v.annotation.consequenceTypes.length > 0) { - for (let j = 0; j < v.annotation.consequenceTypes.length; j++) { - const cT = v.annotation.consequenceTypes[j]; - // gene - if (typeof cT?.geneName === "string" && ct?.geneName !== "") { - genes.add(cT.geneName); + if (variant.annotation) { + // Process the information in the ConsequenceType arrays + if (variant.annotation.consequenceTypes?.length > 0) { + for (const consequenceType of variant.annotation.consequenceTypes) { + // Genes + if (typeof consequenceType?.geneName === "string" && consequenceTypeNames?.geneName !== "") { + genes.add(consequenceType.geneName); } // Consequence Type - for (let z = 0; z < cT.sequenceOntologyTerms.length; z++) { - const consequenceTypeName = cT.sequenceOntologyTerms[z].name; - if (consequenceTypeName !== "") { - ct.add(consequenceTypeName); + for (const consequenceTypeName of consequenceType.sequenceOntologyTerms) { + if (consequenceTypeName.name) { + consequenceTypeNames.add(consequenceTypeName.name); } } - // Sift, Polyphen - if (typeof cT.proteinVariantAnnotation !== "undefined" && - typeof cT.proteinVariantAnnotation.substitutionScores !== "undefined") { - for (let ss = 0; ss < cT.proteinVariantAnnotation.substitutionScores.length; ss++) { - const substitutionScore = cT.proteinVariantAnnotation.substitutionScores[ss]; - const source = substitutionScore.source; - switch (source) { + // Sift, Polyphen, Revel + if (consequenceType.proteinVariantAnnotation?.substitutionScores) { + let siftMin = 10; + let polyphenMax = 0; + let revelMax = 0; + for (const substitutionScore of consequenceType.proteinVariantAnnotation.substitutionScores) { + switch (substitutionScore.source) { case "sift": - if (substitutionScore.score < min) { - min = substitutionScore.score; - description.sift = substitutionScore.description + " (" + substitutionScore.score + ")"; + if (substitutionScore.score < siftMin) { + siftMin = substitutionScore.score; + proteinSubstitutionScores.sift = substitutionScore.description + " (" + substitutionScore.score + ")"; } break; case "polyphen": - if (substitutionScore.score >= max) { - max = substitutionScore.score; - description.polyphen = substitutionScore.description + " (" + substitutionScore.score + ")"; + if (substitutionScore.score >= polyphenMax) { + polyphenMax = substitutionScore.score; + proteinSubstitutionScores.polyphen = substitutionScore.description + " (" + substitutionScore.score + ")"; + } + break; + case "revel": + if (substitutionScore.score >= revelMax) { + revelMax = substitutionScore.score; + proteinSubstitutionScores.revel = substitutionScore.score; } break; } } } - } } // CADD - if (v.annotation?.functionalScore) { - for (let fs = 0; fs < v.annotation.functionalScore.length; fs++) { - if (v.annotation.functionalScore[fs] && v.annotation.functionalScore[fs].source === "cadd_scaled") { - cadd = Number(v.annotation.functionalScore[fs].score).toFixed(2); + if (variant.annotation?.functionalScore) { + for (let fs = 0; fs < variant.annotation.functionalScore.length; fs++) { + if (variant.annotation.functionalScore[fs]?.source === "cadd_scaled") { + cadd = Number(variant.annotation.functionalScore[fs].score).toFixed(2); } } } // Conservation - if (v.annotation?.conservation) { - for (let cons = 0; cons < v.annotation.conservation.length; cons++) { - const conservation = v.annotation.conservation[cons]; + if (variant.annotation?.conservation) { + for (const conservation of variant.annotation.conservation) { switch (conservation.source) { case "phylop": phylop = Number(conservation.score).toFixed(3); @@ -227,19 +231,20 @@ export default class VariantUtils { const populations = []; const populationStudyBidimensional = []; const populationMapExists = []; - studiesPopFrequencies.forEach(study => { + populationFrequenciesStudies.forEach(study => { populations[study.id] = study.populations.map(pop => pop.id); study.populations.forEach(pop => { populationMapExists[pop.id] = true; }); populationStudyBidimensional[study.id] = populationMapExists; }); - if (typeof studiesPopFrequencies !== "undefined" && studiesPopFrequencies.length > 0) { - for (let j = 0; j < studiesPopFrequencies.length; j++) { - const study = studiesPopFrequencies[j]; - for (const popFreqIdx in v.annotation.populationFrequencies) { - if (Object.prototype.hasOwnProperty.call(v.annotation.populationFrequencies, popFreqIdx)) { - const popFreq = v.annotation.populationFrequencies[popFreqIdx]; + + if (populationFrequenciesStudies?.length > 0) { + for (let j = 0; j < populationFrequenciesStudies.length; j++) { + const study = populationFrequenciesStudies[j]; + for (const popFreqIdx in variant.annotation.populationFrequencies) { + if (Object.prototype.hasOwnProperty.call(variant.annotation.populationFrequencies, popFreqIdx)) { + const popFreq = variant.annotation.populationFrequencies[popFreqIdx]; if (UtilsNew.isNotUndefinedOrNull(popFreq)) { const population = popFreq.population; if (study.id === popFreq.study && populationStudyBidimensional[study.id][population] === true) { @@ -251,18 +256,18 @@ export default class VariantUtils { } } - if (v.annotation?.populationFrequencies?.length) { - for (let pf = 0; pf < v.annotation.populationFrequencies.length; pf++) { - const pop = v.annotation.populationFrequencies[pf].study + "_" + v.annotation.populationFrequencies[pf].population; + if (variant.annotation?.populationFrequencies?.length) { + for (let pf = 0; pf < variant.annotation.populationFrequencies.length; pf++) { + const pop = variant.annotation.populationFrequencies[pf].study + "_" + variant.annotation.populationFrequencies[pf].population; if (typeof populationMap[pop] !== "undefined" && populationMap[pop] === "NA") { - populationMap[pop] = Number(v.annotation.populationFrequencies[pf].altAlleleFreq).toFixed(4); + populationMap[pop] = Number(variant.annotation.populationFrequencies[pf].altAlleleFreq).toFixed(4); } } } - // Clinvar, cosmic - if (v.annotation?.traitAssociation?.length) { - v.annotation.traitAssociation.forEach(clinicalData => { + // Clinvar, Cosmic + if (variant.annotation?.traitAssociation?.length) { + variant.annotation.traitAssociation.forEach(clinicalData => { if (clinicalData.source.name === "clinvar") { // Verify isn't undefined const clinicalSignificance = clinicalData?.variantClassification?.clinicalSignificance ? ` (${clinicalData?.variantClassification?.clinicalSignificance})`: ""; @@ -279,82 +284,89 @@ export default class VariantUtils { }); } - genes = genes.size > 0 ? [...genes].join(",") : "-"; - ct = ct.size > 0 ? [...ct].join(",") : "-"; - sift = typeof description.sift !== "undefined" ? description.sift : "-"; - polyphen = typeof description.polyphen !== "undefined" ? description.polyphen : "-"; + // genes = genes.size > 0 ? [...genes].join(",") : "-"; + // consequenceTypeNames = consequenceTypeNames.size > 0 ? [...consequenceTypeNames].join(",") : "-"; + // sift = typeof proteinSubstitutionScores.sift !== "undefined" ? proteinSubstitutionScores.sift : "-"; + // polyphen = typeof proteinSubstitutionScores.polyphen !== "undefined" ? proteinSubstitutionScores.polyphen : "-"; clinvar = clinvar.size > 0 ? [...clinvar].join(",") : "-"; cosmic = cosmic.size > 0 ? [...cosmic.entries()].map(([traitId, histologies]) => traitId + "(" + [...histologies].join(",") + ")").join(",") : "-"; } // prediction - if (v.evidences) { - prediction = this.getClassificationByClinicalSignificance(v); + if (variant.evidences) { + acmgPrediction = this.getClassificationByClinicalSignificance(variant); } - // ID + // START PREPARING THE LINE if (flatFieldList.includes("id")) { - dataToTsv["id"] = v.chromosome + ":" + v.start + " " + v.reference + "/" + v.alternate; - + dataToTsv["id"] = variant.chromosome + ":" + variant.start + ":" + variant.reference || "-" + ":" + variant.alternate || "-"; + } + if (flatFieldList.includes("snp_id")) { // SNP ID - if (v?.id?.startsWith("rs")) { - dataToTsv["SNP_ID"] = v.id; - } else if (typeof v.annotation !== "undefined" && typeof v.annotation.xrefs !== "undefined" && v.annotation.xrefs.length > 0) { - const annotation = v.annotation.xrefs.find(el => el.source === "dbSNP"); - if (typeof annotation !== "undefined") { - dataToTsv["SNP_ID"] = annotation.id; + const dbSnpId = variant.names?.filter(name => name.startsWith("rs"))?.map(name => name).join(","); + if (dbSnpId) { + dataToTsv["snp_id"] = dbSnpId; + } else { + if (variant.annotation?.xrefs?.length > 0) { + const dbSnpXref = variant.annotation.xrefs.find(el => el.source === "dbSNP"); + if (dbSnpXref) { + dataToTsv["snp_id"] = dbSnpXref.id; + } else { + dataToTsv["snp_id"] = "-"; + } } else { - dataToTsv["SNP_ID"] = "-"; + dataToTsv["snp_id"] = "-"; } - } else { - dataToTsv["SNP_ID"] = "-"; } } - - // Genes if (flatFieldList.includes("gene")) { - dataToTsv["gene"] = genes; + dataToTsv["gene"] = genes.size > 0 ? [...genes].join(",") : "-"; } - - // type if (flatFieldList.includes("type")) { - dataToTsv["type"] = v.type; + dataToTsv["type"] = variant.type; } - - // consequence type - if (flatFieldList.includes("consequenceType")) { - dataToTsv["consequenceType"] = ct; + if (flatFieldList.includes("hgvs")) { + dataToTsv["hgvs"] = this.gethgvsValues(variant); } + // Sample if (samples?.length > 0) { - const gtSamples = this.getGenotypeSamples(v, samples, nucleotideGenotype); + const gtSamples = this.getGenotypeSamples(variant, samples, nucleotideGenotype); + const sampleGenotypes = []; gtSamples.forEach(sample => { Object.keys(sample).forEach(sampleId => { - if (flatFieldList.includes("sampleGenotypes." + sampleId)) { - dataToTsv["sampleGenotypes."+ sampleId] = sample[sampleId]; - } - - if (flatFieldList.includes("samples." + sampleId)) { - dataToTsv["samples."+ sampleId] = sample[sampleId]; + // if (flatFieldList.includes("sampleGenotypes." + sampleId)) { + // dataToTsv["sampleGenotypes."+ sampleId] = sample[sampleId]; + // } + // if (flatFieldList.includes("samples." + sampleId)) { + // dataToTsv["samples."+ sampleId] = sample[sampleId]; + // } + if (sample[sampleId] !== "-") { + sampleGenotypes.push(sampleId + ":" + sample[sampleId]); } }); }); + dataToTsv["samples"] = sampleGenotypes.join(","); } - // deleteriousness + if (flatFieldList.includes("consequenceType")) { + dataToTsv["consequenceType"] = consequenceTypeNames.size > 0 ? [...consequenceTypeNames].join(",") : "-"; + } if (flatFieldList.includes("deleteriousness.SIFT")) { - dataToTsv["deleteriousness.SIFT"] = sift; + dataToTsv["deleteriousness.SIFT"] = proteinSubstitutionScores.sift || "-"; } if (flatFieldList.includes("deleteriousness.polyphen")) { - dataToTsv["deleteriousness.polyphen"] = polyphen; + dataToTsv["deleteriousness.polyphen"] = proteinSubstitutionScores.polyphen || "-"; } if (flatFieldList.includes("deleteriousness.revel")) { - row.push("-"); // TODO deleteriousness Revel is missing - dataToTsv["deleteriousness.revel"] = "-"; + dataToTsv["deleteriousness.revel"] = proteinSubstitutionScores.revel || "-"; } if (flatFieldList.includes("deleteriousness.cadd")) { dataToTsv["deleteriousness.cadd"] = cadd; } + if (flatFieldList.includes("deleteriousness.spliceai")) { + dataToTsv["deleteriousness.spliceai"] = this.getSpliceAI(variant); + } if (flatFieldList.includes("conservation.phylop")) { dataToTsv["conservation.phylop"] = phylop; } @@ -364,68 +376,63 @@ export default class VariantUtils { if (flatFieldList.includes("conservation.gerp")) { dataToTsv["conservation.gerp"] = gerp; } - if (flatFieldList.includes("deleteriousness.spliceai")) { - dataToTsv["deleteriousness.spliceai"] = this.getSpliceAI(v); - } - if (flatFieldList.includes("hgvs")) { - dataToTsv["hgvs"] = this.gethgvsValues(v); - } - - // Allele stats (VB) - // frequencies.cohort (SVB) - // alleleStats.forEach(study => { - - // if (flatFieldList.includes(`cohorts.${study.id}`) || flatFieldList.includes("frequencies.cohort")) { - // const ac = []; - // const af = []; - // study.stats.map(cohort => { - // ac.push(`${cohort.cohortId}:${cohort.alleleCount}`); - // af.push(`${cohort.cohortId}:${cohort.altAlleleFreq}`); - // }); - // dataToTsv[`cohorts.${study.id}.alleleCount`] = ac.join(";"); - // dataToTsv[`cohorts.${study.id}.altAlleleFreq`] = af.join(";"); - // } - // }); - - v?.studies.forEach(study => { - const studyId = study.studyId.split(":")[1]; - if (flatFieldList.includes(`cohorts.${studyId}`) || flatFieldList.includes("frequencies.cohort")) { - const ac = []; - const af = []; - study?.stats.map(cohort => { - ac.push(`${cohort.cohortId}:${cohort.alleleCount}`); - af.push(`${cohort.cohortId}:${cohort.altAlleleFreq}`); - }); - dataToTsv[`cohorts.${studyId}.alleleCount`] = ac.join(";"); - dataToTsv[`cohorts.${studyId}.altAlleleFreq`] = af.join(";"); + if (flatFieldList.includes("cohortStats")) { + // variant?.studies.forEach(study => { + // const studyId = study.studyId.split(":")[1]; + // if (flatFieldList.includes(`cohorts.${studyId}`) || flatFieldList.includes("frequencies.cohort")) { + // const ac = []; + // const af = []; + // study?.stats.map(cohort => { + // ac.push(`${cohort.cohortId}:${cohort.alleleCount}`); + // af.push(`${cohort.cohortId}:${cohort.altAlleleFreq}`); + // }); + // dataToTsv[`cohorts.${studyId}.alleleCount`] = ac.join(";"); + // dataToTsv[`cohorts.${studyId}.altAlleleFreq`] = af.join(";"); + // } + // }); + const cohortStats = []; + for (const study of variant.studies) { + const studyId = study.studyId.split(":")[1]; + for (const stats of study.stats) { + cohortStats.push(studyId + ":" + stats.cohortId + "=" + stats.altAlleleFreq); + } } - }); + dataToTsv["cohortStats"] = cohortStats.join(","); + } - studiesPopFrequencies.forEach(study => { - study.populations.forEach(pop => { - if (flatFieldList.includes("popfreq." + study.id) || flatFieldList.includes("frequencies.populationFrequencies")) { + if (flatFieldList.includes("populationFrequencies")) { + // populationFrequenciesStudies.forEach(study => { + // study.populations.forEach(pop => { + // if (flatFieldList.includes("popfreq." + study.id) || flatFieldList.includes("frequencies.populationFrequencies")) { + // const valuePopFreq = populationMap[study.id + "_" + pop.id]; + // dataToTsv[`popfreq.${study.id}_${pop.id}`] = UtilsNew.isNotEmpty(valuePopFreq) ? valuePopFreq : "-"; + // } + // }); + // }); + + const populationFrequencies = []; + for (const study of populationFrequenciesStudies) { + for (const pop of study.populations) { const valuePopFreq = populationMap[study.id + "_" + pop.id]; - dataToTsv[`popfreq.${study.id}_${pop.id}`] = UtilsNew.isNotEmpty(valuePopFreq) ? valuePopFreq : "-"; + populationFrequencies.push(study.id + ":" + pop.id + "=" + valuePopFreq); } - }); - }); - + } + dataToTsv["populationFrequencies"] = populationFrequencies.join(","); + } if (flatFieldList.includes("clinicalInfo.clinvar")) { dataToTsv["clinicalInfo.clinvar"] = clinvar; } - if (flatFieldList.includes("clinicalInfo.cosmic")) { dataToTsv["clinicalInfo.cosmic"] = cosmic; } - if (flatFieldList.includes("interpretation.prediction")) { - dataToTsv["interpretation.prediction"] = prediction; + dataToTsv["acmgPrediction"] = acmgPrediction; } - const rowValues = headerString.map(head => dataToTsv[head]); + + const rowValues = flatFieldList.map(header => dataToTsv[header]); rows.push(rowValues.join("\t")); } - return rows; } @@ -539,7 +546,6 @@ export default class VariantUtils { } static gethgvsValues(variant) { - BioinfoUtils.sort(variant.annotation?.consequenceTypes, v => v.geneName); const gridConfig = { geneSet: { @@ -615,20 +621,6 @@ export default class VariantUtils { return "-"; } - static removeUnlockQuery(lockedFields, preparedQuery, executedQuery) { - // Get all keys - const queryKeys = new Set([...Object.keys(preparedQuery), ...Object.keys(executedQuery)]); - - // Remove keys belong to lockedFields - lockedFields.forEach(key => queryKeys.delete(key.id)); - - // Remove all key not belong to lockedFields - queryKeys.forEach(key => { - delete preparedQuery[key]; - delete executedQuery[key]; - }); - } - static validateQuery(query) { if (!query?.panel) { if ("panelFeatureType" in query) { From be9e7fe6019d88dccf9885d71c0cb2026d35bcf8 Mon Sep 17 00:00:00 2001 From: imedina Date: Wed, 6 Mar 2024 23:35:53 +0000 Subject: [PATCH 2/7] Code cleanup --- src/webcomponents/variant/variant-utils.js | 129 ++++----------------- 1 file changed, 23 insertions(+), 106 deletions(-) diff --git a/src/webcomponents/variant/variant-utils.js b/src/webcomponents/variant/variant-utils.js index e0e801f423..be790d92d8 100644 --- a/src/webcomponents/variant/variant-utils.js +++ b/src/webcomponents/variant/variant-utils.js @@ -24,43 +24,6 @@ export default class VariantUtils { static jsonToTabConvert(variants, populationFrequenciesStudies, samples, nucleotideGenotype, fieldList) { const rows = []; let populationMap = {}; - const headerString = []; - // const sampleIds = samples?.map(sample => sample.id); - - // took from the first result. Is there a better way? - // allele count / allele freqs - // const cohortAlleleStatsColumns = []; - // const alleleStats = []; - // const studyIds = []; - - // variants?.[0]?.studies - // ?.filter(s => s.studyId.includes("@")) - // .map(s => s.studyId.split(":")[1]); - - // Code to Remove - // ### - // if (variants[0].studies?.length) { - // variants[0].studies.forEach(study => { - // if (study.studyId.includes("@")) { - // const studyId = study.studyId.split(":")[1]; - // studyIds.push(studyId); - // cohortAlleleStatsColumns.push(`cohorts.${studyId}.alleleCount`, `cohorts.${studyId}.altAlleleFreq`); - // // alleleCount, altAlleleFreq - // - // // cohort ALL is always the first element in study.stats - // // Remove - // alleleStats.push({ - // id: studyId, - // stats: study.stats, - // }); - // } else { - // console.error("Unexpected studyId format"); - // } - // }); - // } - // ##### - - // const popStudyIds = populationFrequenciesStudies?.map(popFreqStudy => "popfreq." + popFreqStudy.id); /* // explicit list gives less maintainability but we need customisation (also in some cases for each column there is more than 1 field) */ let flatFieldList = []; @@ -74,8 +37,6 @@ export default class VariantUtils { "gene", "type", "hgvs", - // Adding SAMPLES (includeSample=all in VB and Case samples in Sample VB) - // ...samples.map(sample => sample.id || sample), "samples", "consequenceType", "deleteriousness.SIFT", @@ -86,14 +47,7 @@ export default class VariantUtils { "conservation.phylop", "conservation.phastCons", "conservation.gerp", - // AC / AF - // fieldList (columns in the grid) is in the form: cohorts.RD38, cohorts.CG38 - // TSV in the form: cohort.RD38.alleleCount,cohort.RD38.altAlleleFreq - // ...studyIds.map(studyId => `cohorts.${studyId}`), "cohortStats", - // fieldList (columns in the grid) is in the form: popfreq.1kG_phase3, popfreq.GNOMAD_GENOMES - // TSV in the form: popfreq.1kG_phase3_SAS,popfreq.GNOMAD_GENOMES_ALL,popfreq.GNOMAD_GENOMES_AFR - // ...popStudyIds, "populationFrequencies", "clinicalInfo.clinvar", "clinicalInfo.cosmic", @@ -103,39 +57,8 @@ export default class VariantUtils { flatFieldList = fieldList .filter(f => f.export && !f.excludeFromExport) .flatMap(f => f.children?.filter(f => f.export && !f.excludeFromExport).map(x => f.id + "." + x.id) ?? f.id); - // ESlint parse error. Cannot read property 'range' of null https://github.com/babel/babel-eslint/issues/681 - // flatFieldList = fieldList.filter(f => f.export).flatMap(f => f.children?.filter(f => f.export).map(x => `${f.id}.${x.id}`) ?? f.id); } - // flatFieldList.forEach(f => { - // if ("id" === f) { - // headerString.push("id"); - // // headerString.push("snp_id"); - // } else if (f.startsWith("cohorts.")) { - // // Cohorts Variant Browser - // studyIds.forEach(id => { - // if (f === "cohorts." + id) { - // headerString.push(`cohorts.${id}.alleleCount`, `cohorts.${id}.altAlleleFreq`); - // } - // }); - // } else if ("frequencies.cohort" === f) { - // // Cohorts in Sample Variant Browser - // studyIds.forEach(id => headerString.push(`cohorts.${id}.alleleCount`, `cohorts.${id}.altAlleleFreq`)); - // } else if (f.startsWith("popfreq.")) { - // // Pop freq in Variant Browser - // populationFrequenciesStudies.forEach(study => { - // if (f === "popfreq." + study.id) { - // headerString.push(...study.populations.map(pop => "popfreq." + study.id + "_" + pop.id)); - // } - // }); - // } else if ("frequencies.populationFrequencies" === f) { - // // Pop freq in Sample Variant Browser - // populationFrequenciesStudies.forEach(study => headerString.push(...study.populations.map(pop => "popfreq." + study.id + "_" + pop.id))); - // } else { - // headerString.push(f); - // } - // }); - // TSV header rows.push(flatFieldList.join("\t")); @@ -163,7 +86,7 @@ export default class VariantUtils { genes.add(consequenceType.geneName); } - // Consequence Type + // Consequence Types for (const consequenceTypeName of consequenceType.sequenceOntologyTerms) { if (consequenceTypeName.name) { consequenceTypeNames.add(consequenceTypeName.name); @@ -284,10 +207,6 @@ export default class VariantUtils { }); } - // genes = genes.size > 0 ? [...genes].join(",") : "-"; - // consequenceTypeNames = consequenceTypeNames.size > 0 ? [...consequenceTypeNames].join(",") : "-"; - // sift = typeof proteinSubstitutionScores.sift !== "undefined" ? proteinSubstitutionScores.sift : "-"; - // polyphen = typeof proteinSubstitutionScores.polyphen !== "undefined" ? proteinSubstitutionScores.polyphen : "-"; clinvar = clinvar.size > 0 ? [...clinvar].join(",") : "-"; cosmic = cosmic.size > 0 ? [...cosmic.entries()].map(([traitId, histologies]) => traitId + "(" + [...histologies].join(",") + ")").join(",") : "-"; } @@ -297,13 +216,19 @@ export default class VariantUtils { acmgPrediction = this.getClassificationByClinicalSignificance(variant); } + // START PREPARING THE LINE if (flatFieldList.includes("id")) { - dataToTsv["id"] = variant.chromosome + ":" + variant.start + ":" + variant.reference || "-" + ":" + variant.alternate || "-"; + dataToTsv["id"] = variant.chromosome + ":" + variant.start + ":" + (variant.reference || "-") + (":" + variant.alternate || "-"); } + if (flatFieldList.includes("snp_id")) { // SNP ID - const dbSnpId = variant.names?.filter(name => name.startsWith("rs"))?.map(name => name).join(","); + const dbSnpId = variant.names + ?.filter(name => name.startsWith("rs")) + ?.map(name => name) + .join(","); + if (dbSnpId) { dataToTsv["snp_id"] = dbSnpId; } else { @@ -319,12 +244,15 @@ export default class VariantUtils { } } } + if (flatFieldList.includes("gene")) { dataToTsv["gene"] = genes.size > 0 ? [...genes].join(",") : "-"; } + if (flatFieldList.includes("type")) { dataToTsv["type"] = variant.type; } + if (flatFieldList.includes("hgvs")) { dataToTsv["hgvs"] = this.gethgvsValues(variant); } @@ -352,45 +280,40 @@ export default class VariantUtils { if (flatFieldList.includes("consequenceType")) { dataToTsv["consequenceType"] = consequenceTypeNames.size > 0 ? [...consequenceTypeNames].join(",") : "-"; } + if (flatFieldList.includes("deleteriousness.SIFT")) { dataToTsv["deleteriousness.SIFT"] = proteinSubstitutionScores.sift || "-"; } + if (flatFieldList.includes("deleteriousness.polyphen")) { dataToTsv["deleteriousness.polyphen"] = proteinSubstitutionScores.polyphen || "-"; } + if (flatFieldList.includes("deleteriousness.revel")) { dataToTsv["deleteriousness.revel"] = proteinSubstitutionScores.revel || "-"; } + if (flatFieldList.includes("deleteriousness.cadd")) { dataToTsv["deleteriousness.cadd"] = cadd; } + if (flatFieldList.includes("deleteriousness.spliceai")) { dataToTsv["deleteriousness.spliceai"] = this.getSpliceAI(variant); } + if (flatFieldList.includes("conservation.phylop")) { dataToTsv["conservation.phylop"] = phylop; } + if (flatFieldList.includes("conservation.phastCons")) { dataToTsv["conservation.phastCons"] = phastCons; } + if (flatFieldList.includes("conservation.gerp")) { dataToTsv["conservation.gerp"] = gerp; } if (flatFieldList.includes("cohortStats")) { - // variant?.studies.forEach(study => { - // const studyId = study.studyId.split(":")[1]; - // if (flatFieldList.includes(`cohorts.${studyId}`) || flatFieldList.includes("frequencies.cohort")) { - // const ac = []; - // const af = []; - // study?.stats.map(cohort => { - // ac.push(`${cohort.cohortId}:${cohort.alleleCount}`); - // af.push(`${cohort.cohortId}:${cohort.altAlleleFreq}`); - // }); - // dataToTsv[`cohorts.${studyId}.alleleCount`] = ac.join(";"); - // dataToTsv[`cohorts.${studyId}.altAlleleFreq`] = af.join(";"); - // } - // }); const cohortStats = []; for (const study of variant.studies) { const studyId = study.studyId.split(":")[1]; @@ -402,15 +325,6 @@ export default class VariantUtils { } if (flatFieldList.includes("populationFrequencies")) { - // populationFrequenciesStudies.forEach(study => { - // study.populations.forEach(pop => { - // if (flatFieldList.includes("popfreq." + study.id) || flatFieldList.includes("frequencies.populationFrequencies")) { - // const valuePopFreq = populationMap[study.id + "_" + pop.id]; - // dataToTsv[`popfreq.${study.id}_${pop.id}`] = UtilsNew.isNotEmpty(valuePopFreq) ? valuePopFreq : "-"; - // } - // }); - // }); - const populationFrequencies = []; for (const study of populationFrequenciesStudies) { for (const pop of study.populations) { @@ -420,12 +334,15 @@ export default class VariantUtils { } dataToTsv["populationFrequencies"] = populationFrequencies.join(","); } + if (flatFieldList.includes("clinicalInfo.clinvar")) { dataToTsv["clinicalInfo.clinvar"] = clinvar; } + if (flatFieldList.includes("clinicalInfo.cosmic")) { dataToTsv["clinicalInfo.cosmic"] = cosmic; } + if (flatFieldList.includes("interpretation.prediction")) { dataToTsv["acmgPrediction"] = acmgPrediction; } From f05d9e0541174e2bd440aea2968cafded04eab96 Mon Sep 17 00:00:00 2001 From: imedina Date: Fri, 8 Mar 2024 01:49:12 +0000 Subject: [PATCH 3/7] Several minor fixes and improvements --- src/webcomponents/variant/variant-utils.js | 75 ++++++++++++---------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/webcomponents/variant/variant-utils.js b/src/webcomponents/variant/variant-utils.js index be790d92d8..bf5cf791a7 100644 --- a/src/webcomponents/variant/variant-utils.js +++ b/src/webcomponents/variant/variant-utils.js @@ -33,12 +33,12 @@ export default class VariantUtils { // default list flatFieldList = [ "id", - "snp_id", + "snpId", "gene", "type", "hgvs", "samples", - "consequenceType", + "consequenceTypes", "deleteriousness.SIFT", "deleteriousness.polyphen", "deleteriousness.revel", @@ -47,7 +47,7 @@ export default class VariantUtils { "conservation.phylop", "conservation.phastCons", "conservation.gerp", - "cohortStats", + "cohortStats.alt", "populationFrequencies", "clinicalInfo.clinvar", "clinicalInfo.cosmic", @@ -65,14 +65,14 @@ export default class VariantUtils { for (const variant of variants) { const genes = new Set(); const consequenceTypeNames = new Set(); - const proteinSubstitutionScores = {sift: "-", polyphen: "-", revel: "-"}; - let cadd = "-"; - let phylop = "-"; - let phastCons = "-"; - let gerp = "-"; + const proteinSubstitutionScores = {sift: "", polyphen: "", revel: ""}; + let cadd = ""; + let phylop = ""; + let phastCons = ""; + let gerp = ""; let clinvar = new Set(); let cosmic = new Map(); - let acmgPrediction = "-"; + let acmgPrediction = ""; populationMap = {}; const dataToTsv = {}; @@ -89,7 +89,11 @@ export default class VariantUtils { // Consequence Types for (const consequenceTypeName of consequenceType.sequenceOntologyTerms) { if (consequenceTypeName.name) { - consequenceTypeNames.add(consequenceTypeName.name); + let ct = consequenceTypeName.name; + if (consequenceType.geneName) { + ct += "(" + consequenceType.geneName + ")"; + } + consequenceTypeNames.add(ct); } } @@ -207,8 +211,8 @@ export default class VariantUtils { }); } - clinvar = clinvar.size > 0 ? [...clinvar].join(",") : "-"; - cosmic = cosmic.size > 0 ? [...cosmic.entries()].map(([traitId, histologies]) => traitId + "(" + [...histologies].join(",") + ")").join(",") : "-"; + clinvar = clinvar.size > 0 ? [...clinvar].join(",") : ""; + cosmic = cosmic.size > 0 ? [...cosmic.entries()].map(([traitId, histologies]) => traitId + "(" + [...histologies].join(",") + ")").join(",") : ""; } // prediction @@ -222,7 +226,7 @@ export default class VariantUtils { dataToTsv["id"] = variant.chromosome + ":" + variant.start + ":" + (variant.reference || "-") + (":" + variant.alternate || "-"); } - if (flatFieldList.includes("snp_id")) { + if (flatFieldList.includes("snpId")) { // SNP ID const dbSnpId = variant.names ?.filter(name => name.startsWith("rs")) @@ -230,23 +234,23 @@ export default class VariantUtils { .join(","); if (dbSnpId) { - dataToTsv["snp_id"] = dbSnpId; + dataToTsv["snpId"] = dbSnpId; } else { if (variant.annotation?.xrefs?.length > 0) { const dbSnpXref = variant.annotation.xrefs.find(el => el.source === "dbSNP"); if (dbSnpXref) { - dataToTsv["snp_id"] = dbSnpXref.id; + dataToTsv["snpId"] = dbSnpXref.id; } else { - dataToTsv["snp_id"] = "-"; + dataToTsv["snpId"] = ""; } } else { - dataToTsv["snp_id"] = "-"; + dataToTsv["snpId"] = ""; } } } if (flatFieldList.includes("gene")) { - dataToTsv["gene"] = genes.size > 0 ? [...genes].join(",") : "-"; + dataToTsv["gene"] = genes.size > 0 ? [...genes].join(",") : ""; } if (flatFieldList.includes("type")) { @@ -263,12 +267,6 @@ export default class VariantUtils { const sampleGenotypes = []; gtSamples.forEach(sample => { Object.keys(sample).forEach(sampleId => { - // if (flatFieldList.includes("sampleGenotypes." + sampleId)) { - // dataToTsv["sampleGenotypes."+ sampleId] = sample[sampleId]; - // } - // if (flatFieldList.includes("samples." + sampleId)) { - // dataToTsv["samples."+ sampleId] = sample[sampleId]; - // } if (sample[sampleId] !== "-") { sampleGenotypes.push(sampleId + ":" + sample[sampleId]); } @@ -277,20 +275,20 @@ export default class VariantUtils { dataToTsv["samples"] = sampleGenotypes.join(","); } - if (flatFieldList.includes("consequenceType")) { - dataToTsv["consequenceType"] = consequenceTypeNames.size > 0 ? [...consequenceTypeNames].join(",") : "-"; + if (flatFieldList.includes("consequenceTypes")) { + dataToTsv["consequenceTypes"] = consequenceTypeNames.size > 0 ? [...consequenceTypeNames].join(",") : ""; } if (flatFieldList.includes("deleteriousness.SIFT")) { - dataToTsv["deleteriousness.SIFT"] = proteinSubstitutionScores.sift || "-"; + dataToTsv["deleteriousness.SIFT"] = proteinSubstitutionScores.sift || ""; } if (flatFieldList.includes("deleteriousness.polyphen")) { - dataToTsv["deleteriousness.polyphen"] = proteinSubstitutionScores.polyphen || "-"; + dataToTsv["deleteriousness.polyphen"] = proteinSubstitutionScores.polyphen || ""; } if (flatFieldList.includes("deleteriousness.revel")) { - dataToTsv["deleteriousness.revel"] = proteinSubstitutionScores.revel || "-"; + dataToTsv["deleteriousness.revel"] = proteinSubstitutionScores.revel || ""; } if (flatFieldList.includes("deleteriousness.cadd")) { @@ -313,7 +311,7 @@ export default class VariantUtils { dataToTsv["conservation.gerp"] = gerp; } - if (flatFieldList.includes("cohortStats")) { + if (flatFieldList.includes("cohortStats.alt")) { const cohortStats = []; for (const study of variant.studies) { const studyId = study.studyId.split(":")[1]; @@ -321,7 +319,7 @@ export default class VariantUtils { cohortStats.push(studyId + ":" + stats.cohortId + "=" + stats.altAlleleFreq); } } - dataToTsv["cohortStats"] = cohortStats.join(","); + dataToTsv["cohortStats.alt"] = cohortStats.join(","); } if (flatFieldList.includes("populationFrequencies")) { @@ -329,7 +327,9 @@ export default class VariantUtils { for (const study of populationFrequenciesStudies) { for (const pop of study.populations) { const valuePopFreq = populationMap[study.id + "_" + pop.id]; - populationFrequencies.push(study.id + ":" + pop.id + "=" + valuePopFreq); + if (valuePopFreq) { + populationFrequencies.push(study.id + ":" + pop.id + "=" + valuePopFreq); + } } } dataToTsv["populationFrequencies"] = populationFrequencies.join(","); @@ -488,10 +488,15 @@ export default class VariantUtils { const results = []; for (const index of showArrayIndexes) { const consequenceType = variant.annotation.consequenceTypes[index]; + const hgvsTranscriptIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.transcriptId)); + if (hgvsTranscriptIndex > -1) { + results.push(`${this.getHgvs(consequenceType.transcriptId, variant.annotation.hgvs) || ""}`); + } + const hgvsProteingIndex = variant.annotation.hgvs.findIndex(hgvs => hgvs.startsWith(consequenceType.proteinVariantAnnotation?.proteinId)); - if (hgvsTranscriptIndex > -1 || hgvsProteingIndex > -1) { - results.push(`${this.getHgvs(consequenceType.transcriptId, variant.annotation.hgvs) || "-"} ${this.getHgvs(consequenceType.proteinVariantAnnotation?.proteinId, variant.annotation.hgvs) || "-"}`); + if (hgvsProteingIndex > -1) { + results.push(`${this.getHgvs(consequenceType.proteinVariantAnnotation?.proteinId, variant.annotation.hgvs) || ""}`); } } return results.join(); @@ -535,7 +540,7 @@ export default class VariantUtils { } return dscore; } - return "-"; + return ""; } static validateQuery(query) { From 012f67f0b3303362d3bde5618022ce5503fb6ddf Mon Sep 17 00:00:00 2001 From: imedina Date: Fri, 8 Mar 2024 01:55:03 +0000 Subject: [PATCH 4/7] Add temporary fix to search by SNP ID --- .../variant/variant-browser-grid.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/webcomponents/variant/variant-browser-grid.js b/src/webcomponents/variant/variant-browser-grid.js index 5efa87051b..9ff1d9904c 100644 --- a/src/webcomponents/variant/variant-browser-grid.js +++ b/src/webcomponents/variant/variant-browser-grid.js @@ -211,6 +211,34 @@ export default class VariantBrowserGrid extends LitElement { // summary: !this.query.sample && !this.query.family, ...this.query }; + + // TASK-5791: Temporary SNP ID Search fix + if (this.query.xref) { + const snpIds = this.query.xref.split(",").filter(xref => xref.startsWith("rs")); + if (snpIds.length > 0) { + const snpRegion = []; + const request = new XMLHttpRequest(); + for (const snpId of snpIds) { + const url = `https://rest.ensembl.org/variation/human/${snpId}?content-type=application/json`; + + request.onload = event => { + if (request.status === 200) { + const restObject = JSON.parse(event.currentTarget.response); + const mapping = restObject.mappings?.find(m => m.assembly_name === "GRCh38"); + snpRegion.push(mapping.seq_region_name + ":" + mapping.start); + } + }; + request.open("GET", url, false); + request.send(); + } + if (this.filters.region) { + this.filters.region += "," + snpRegion.join(","); + } else { + this.filters.region = snpRegion.join(","); + } + } + } + this.opencgaSession.opencgaClient.variants().query(this.filters) .then(res => { // FIXME A quick temporary fix -> TASK-947 From d931819f69ab191ef9323275e65c691360544a32 Mon Sep 17 00:00:00 2001 From: JuanfeSanahuja Date: Sat, 9 Mar 2024 11:45:33 +0100 Subject: [PATCH 5/7] 2.12.3-1-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 478880079e..082d1fba93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsorolla", - "version": "2.12.3", + "version": "2.12.3-1-dev", "description": "JSorolla is a JavaScript bioinformatic library for data analysis and genomic visualization", "repository": { "type": "git", From 1935cae2294b58de85bc23faa38f377b368743be Mon Sep 17 00:00:00 2001 From: Josemi Date: Mon, 11 Mar 2024 11:58:26 +0100 Subject: [PATCH 6/7] wc: remove onFieldChange listener in steiner-report component #TASK-5691 --- src/webcomponents/variant/custom/steiner-report.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/webcomponents/variant/custom/steiner-report.js b/src/webcomponents/variant/custom/steiner-report.js index 35342af8b9..85b2efd6c9 100644 --- a/src/webcomponents/variant/custom/steiner-report.js +++ b/src/webcomponents/variant/custom/steiner-report.js @@ -291,13 +291,6 @@ class SteinerReport extends LitElement { } } - onFieldChange() { - this._data = { - ...this._data, - }; - this.requestUpdate(); - } - onSignatureChange(event, type) { this.selectedSignatures[type] = event.detail.value; this._config = { @@ -365,7 +358,6 @@ class SteinerReport extends LitElement { From bf48a44d913c4e57e4b5b95535875a57a2d0df15 Mon Sep 17 00:00:00 2001 From: Josemi Date: Wed, 13 Mar 2024 13:10:11 +0100 Subject: [PATCH 7/7] Prepare release v2.12.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 082d1fba93..72e739385d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsorolla", - "version": "2.12.3-1-dev", + "version": "2.12.3-1", "description": "JSorolla is a JavaScript bioinformatic library for data analysis and genomic visualization", "repository": { "type": "git",